modular ui rework
This commit is contained in:
@@ -9,14 +9,15 @@ use openxr as xr;
|
||||
|
||||
use glam::{Affine3A, Vec2, Vec3A};
|
||||
use idmap::IdMap;
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
overlays::{
|
||||
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};
|
||||
@@ -56,8 +57,15 @@ where
|
||||
crate::overlays::screen::get_screens_x11(&app.session)?
|
||||
};
|
||||
|
||||
let mut watch = create_watch::<T>(app, &screens)?;
|
||||
log::info!("Watch Rotation: {:?}", watch.state.spawn_rotation);
|
||||
app.screens.clear();
|
||||
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;
|
||||
overlays.insert(watch.state.id, watch);
|
||||
|
||||
@@ -147,13 +155,14 @@ where
|
||||
}
|
||||
// toggle watch back on if it was hidden
|
||||
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 {
|
||||
Id(usize),
|
||||
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 {
|
||||
Global(Box<dyn FnOnce(&mut AppState) + Send>),
|
||||
Overlay(
|
||||
OverlaySelector,
|
||||
Box<dyn FnOnce(&mut AppState, &mut OverlayState) + Send>,
|
||||
),
|
||||
CreateOverlay(
|
||||
OverlaySelector,
|
||||
Box<dyn FnOnce(&mut AppState) -> Option<(OverlayState, Box<dyn OverlayBackend>)> + Send>,
|
||||
),
|
||||
Overlay(OverlaySelector, Box<OverlayTask>),
|
||||
CreateOverlay(OverlaySelector, Box<CreateOverlayTask>),
|
||||
DropOverlay(OverlaySelector),
|
||||
System(SystemTask),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Copy)]
|
||||
pub enum ColorChannel {
|
||||
R,
|
||||
G,
|
||||
B,
|
||||
All,
|
||||
}
|
||||
|
||||
pub struct TaskContainer {
|
||||
|
||||
@@ -143,7 +143,6 @@ impl NotificationManager {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
log::info!("Received notification message: {}", json_str);
|
||||
let msg = match serde_json::from_str::<XsoMessage>(json_str) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use std::ffi::CStr;
|
||||
|
||||
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 crate::backend::common::BackendError;
|
||||
use crate::backend::common::{BackendError, ColorChannel};
|
||||
|
||||
pub trait Affine3AConvert {
|
||||
fn from_affine(affine: Affine3A) -> Self;
|
||||
@@ -96,3 +98,92 @@ impl From<OVRError> for BackendError {
|
||||
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::{
|
||||
backend::{
|
||||
common::SystemTask,
|
||||
input::interact,
|
||||
notifications::NotificationManager,
|
||||
openvr::{
|
||||
helpers::adjust_gain,
|
||||
input::{set_action_manifest, OpenVrInputSource},
|
||||
lines::LinePool,
|
||||
manifest::{install_manifest, uninstall_manifest},
|
||||
@@ -64,11 +66,11 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
|
||||
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 input_mngr = context.input_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 compositor_mngr = context.compositor_mngr();
|
||||
|
||||
@@ -185,6 +187,17 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
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 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;
|
||||
|
||||
@@ -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) {
|
||||
self.offset = 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
|
||||
delete_queue.push((o, cur_frame + 5));
|
||||
}
|
||||
TaskType::System(_task) => {
|
||||
// Not implemented
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ pub struct OverlayState {
|
||||
pub alpha: f32,
|
||||
pub transform: Affine3A,
|
||||
pub saved_point: Option<Vec3A>,
|
||||
pub saved_scale: Option<f32>,
|
||||
pub spawn_scale: f32, // aka width
|
||||
pub spawn_point: Vec3A,
|
||||
pub spawn_rotation: Quat,
|
||||
@@ -52,6 +53,7 @@ impl Default for OverlayState {
|
||||
alpha: 1.0,
|
||||
relative_to: RelativeTo::None,
|
||||
saved_point: None,
|
||||
saved_scale: None,
|
||||
spawn_scale: 1.0,
|
||||
spawn_point: Vec3A::NEG_Z,
|
||||
spawn_rotation: Quat::IDENTITY,
|
||||
@@ -97,10 +99,11 @@ impl OverlayState {
|
||||
|
||||
pub fn auto_movement(&mut self, app: &mut AppState) {
|
||||
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);
|
||||
self.transform = parent
|
||||
* Affine3A::from_scale_rotation_translation(
|
||||
Vec3::ONE * self.spawn_scale,
|
||||
Vec3::ONE * scale,
|
||||
self.spawn_rotation,
|
||||
point.into(),
|
||||
);
|
||||
@@ -110,13 +113,12 @@ impl OverlayState {
|
||||
}
|
||||
|
||||
pub fn reset(&mut self, app: &mut AppState, hard_reset: bool) {
|
||||
let scale = if hard_reset {
|
||||
if hard_reset {
|
||||
self.saved_point = None;
|
||||
self.spawn_scale
|
||||
} else {
|
||||
self.transform.x_axis.length()
|
||||
};
|
||||
self.saved_scale = None;
|
||||
}
|
||||
|
||||
let scale = self.saved_scale.unwrap_or(self.spawn_scale);
|
||||
let point = self.saved_point.unwrap_or(self.spawn_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 {
|
||||
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(
|
||||
Vec2::X,
|
||||
Vec2::NEG_Y * (extent[0] as f32 / extent[1] as f32),
|
||||
center,
|
||||
)
|
||||
}
|
||||
Affine2::from_cols(
|
||||
Vec2::X,
|
||||
Vec2::NEG_Y * (extent[0] as f32 / extent[1] as f32),
|
||||
center,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,13 +2,26 @@ use std::sync::Arc;
|
||||
|
||||
use crate::config_io;
|
||||
use crate::config_io::get_conf_d_path;
|
||||
use crate::gui::modular::ModularUiConfig;
|
||||
use crate::load_with_fallback;
|
||||
use crate::overlays::keyboard;
|
||||
use crate::overlays::watch::WatchConfig;
|
||||
use crate::state::LeftRight;
|
||||
use anyhow::bail;
|
||||
use log::error;
|
||||
use serde::Deserialize;
|
||||
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)> {
|
||||
Vec::new()
|
||||
}
|
||||
@@ -51,9 +64,24 @@ fn def_auto() -> Arc<str> {
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
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")]
|
||||
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")]
|
||||
pub keyboard_sound_enabled: bool,
|
||||
|
||||
@@ -63,15 +91,15 @@ pub struct GeneralConfig {
|
||||
#[serde(default = "def_one")]
|
||||
pub desktop_view_scale: f32,
|
||||
|
||||
#[serde(default = "def_one")]
|
||||
pub watch_scale: f32,
|
||||
|
||||
#[serde(default = "def_half")]
|
||||
pub watch_view_angle_min: f32,
|
||||
|
||||
#[serde(default = "def_point7")]
|
||||
pub watch_view_angle_max: f32,
|
||||
|
||||
#[serde(default = "def_one")]
|
||||
pub long_press_duration: f32,
|
||||
|
||||
#[serde(default = "def_pw_tokens")]
|
||||
pub pw_tokens: Vec<(String, String)>,
|
||||
|
||||
@@ -110,18 +138,54 @@ impl GeneralConfig {
|
||||
fn post_load(&self) {
|
||||
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("watch_scale", self.watch_scale, 0.0, 5.0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_keyboard() -> keyboard::Layout {
|
||||
let yaml_data = load_with_fallback!("keyboard.yaml", "res/keyboard.yaml");
|
||||
serde_yaml::from_str(&yaml_data).expect("Failed to parse keyboard.yaml")
|
||||
const FALLBACKS: [&str; 3] = [
|
||||
include_str!("res/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 {
|
||||
let yaml_data = load_with_fallback!("watch.yaml", "res/watch.yaml");
|
||||
serde_yaml::from_str(&yaml_data).expect("Failed to parse watch.yaml")
|
||||
pub fn load_known_yaml<T>(config_type: ConfigType) -> T
|
||||
where
|
||||
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 {
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::{
|
||||
};
|
||||
|
||||
pub mod font;
|
||||
pub mod modular;
|
||||
|
||||
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,
|
||||
overlay::{OverlayData, OverlayState},
|
||||
},
|
||||
config,
|
||||
config::{self, ConfigType},
|
||||
gui::{color_parse, CanvasBuilder, Control},
|
||||
hid::{KeyModifier, VirtualKey, ALT, CTRL, KEYS_TO_MODS, META, SHIFT, SUPER},
|
||||
state::AppState,
|
||||
@@ -268,7 +268,7 @@ pub struct Layout {
|
||||
|
||||
impl 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
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod custom;
|
||||
pub mod keyboard;
|
||||
#[cfg(feature = "wayland")]
|
||||
pub mod mirror;
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::{
|
||||
io::Cursor,
|
||||
ops::Add,
|
||||
sync::{atomic::AtomicUsize, Arc},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use rodio::{Decoder, Source};
|
||||
@@ -55,19 +56,24 @@ impl Toast {
|
||||
self
|
||||
}
|
||||
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 name: Arc<str> = format!("toast-{}", auto_increment).into();
|
||||
let selector = OverlaySelector::Name(name.clone());
|
||||
|
||||
let destroy_at =
|
||||
std::time::Instant::now().add(std::time::Duration::from_secs_f32(self.timeout));
|
||||
let destroy_at = instant.add(std::time::Duration::from_secs_f32(self.timeout));
|
||||
|
||||
let has_sound = self.sound;
|
||||
|
||||
app.tasks.enqueue(TaskType::CreateOverlay(
|
||||
selector.clone(),
|
||||
Box::new(move |app| new_toast(self, name, app)),
|
||||
));
|
||||
app.tasks.enqueue_at(
|
||||
TaskType::CreateOverlay(
|
||||
selector.clone(),
|
||||
Box::new(move |app| new_toast(self, name, app)),
|
||||
),
|
||||
instant,
|
||||
);
|
||||
|
||||
app.tasks
|
||||
.enqueue_at(TaskType::DropOverlay(selector), destroy_at);
|
||||
|
||||
@@ -1,907 +1,49 @@
|
||||
use std::{
|
||||
f32::consts::PI,
|
||||
io::{Cursor, Read},
|
||||
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 glam::{Quat, Vec3A};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{
|
||||
backend::{
|
||||
common::{OverlaySelector, TaskType},
|
||||
input::PointerMode,
|
||||
overlay::{ui_transform, OverlayData, OverlayState, RelativeTo},
|
||||
},
|
||||
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.,
|
||||
backend::overlay::{ui_transform, OverlayData, OverlayState, RelativeTo},
|
||||
config::{def_left, def_watch_pos, def_watch_rot, load_known_yaml, ConfigType},
|
||||
config_io,
|
||||
gui::modular::{modular_canvas, ModularUiConfig},
|
||||
state::{AppState, LeftRight},
|
||||
};
|
||||
|
||||
pub const WATCH_NAME: &str = "watch";
|
||||
pub const WATCH_SCALE: f32 = 0.11;
|
||||
|
||||
pub fn create_watch<O>(
|
||||
state: &AppState,
|
||||
screens: &[OverlayData<O>],
|
||||
) -> anyhow::Result<OverlayData<O>>
|
||||
pub fn create_watch<O>(state: &AppState) -> anyhow::Result<OverlayData<O>>
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
let config = load_watch();
|
||||
let config = load_known_yaml::<ModularUiConfig>(ConfigType::Watch);
|
||||
|
||||
let mut canvas = CanvasBuilder::new(
|
||||
config.watch_size[0] as _,
|
||||
config.watch_size[1] as _,
|
||||
state.graphics.clone(),
|
||||
state.format,
|
||||
(),
|
||||
)?;
|
||||
let empty_str: Arc<str> = Arc::from("");
|
||||
let canvas = modular_canvas(&config.size, &config.elements, state)?;
|
||||
|
||||
for elem in config.watch_elements.into_iter() {
|
||||
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);
|
||||
let relative_to = RelativeTo::Hand(state.session.config.watch_hand as usize);
|
||||
|
||||
Ok(OverlayData {
|
||||
state: OverlayState {
|
||||
name: WATCH_NAME.into(),
|
||||
want_visible: true,
|
||||
interactable: true,
|
||||
spawn_scale: WATCH_SCALE * state.session.config.watch_scale,
|
||||
spawn_point: state.session.watch_pos.into(),
|
||||
spawn_rotation: state.session.watch_rot,
|
||||
interaction_transform: ui_transform(&config.watch_size),
|
||||
spawn_scale: config.width,
|
||||
spawn_point: Vec3A::from_slice(&state.session.config.watch_pos),
|
||||
spawn_rotation: Quat::from_slice(&state.session.config.watch_rot),
|
||||
interaction_transform: ui_transform(&config.size),
|
||||
relative_to,
|
||||
..Default::default()
|
||||
},
|
||||
backend: Box::new(canvas.build()),
|
||||
backend: Box::new(canvas),
|
||||
..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>)
|
||||
where
|
||||
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;
|
||||
return;
|
||||
}
|
||||
@@ -925,3 +67,33 @@ where
|
||||
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:
|
||||
# - try to re-use font sizes, every loaded font size uses additional VRAM.
|
||||
|
||||
# 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
|
||||
# lookng to make changes?
|
||||
# drop me in ~/.config/wlxoverlay.settings.yaml
|
||||
#
|
||||
# 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
|
||||
watch_hand: Left
|
||||
|
||||
# TODO
|
||||
watch_offset: []
|
||||
|
||||
# TODO
|
||||
watch_rotation: []
|
||||
|
||||
# TODO
|
||||
watch_size: [400, 200]
|
||||
|
||||
watch_elements:
|
||||
elements:
|
||||
# background panel
|
||||
- type: Panel
|
||||
rect: [0, 0, 400, 200]
|
||||
bg_color: "#353535"
|
||||
|
||||
- type: FuncButton
|
||||
- type: Button
|
||||
rect: [2, 162, 26, 36]
|
||||
font_size: 14
|
||||
bg_color: "#808040"
|
||||
fg_color: "#ffffff"
|
||||
func: SwitchWatchHand
|
||||
func_right: HideWatch
|
||||
func_middle: ~
|
||||
text: "W"
|
||||
text: "C"
|
||||
click_up:
|
||||
- type: Window
|
||||
target: settings
|
||||
action: ShowUi
|
||||
|
||||
- type: KeyboardButton
|
||||
# Keyboard button
|
||||
- type: Button
|
||||
rect: [32, 162, 60, 36]
|
||||
font_size: 14
|
||||
fg_color: "#FFFFFF"
|
||||
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
|
||||
- type: OverlayList
|
||||
@@ -64,109 +64,100 @@ watch_elements:
|
||||
fg_color: "#FFFFFF"
|
||||
bg_color: "#405060"
|
||||
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
|
||||
- type: Clock
|
||||
# local clock
|
||||
- type: Label
|
||||
rect: [19, 90, 200, 50]
|
||||
#format: "%h:%M %p" # 11:59 PM
|
||||
format: "%H:%M" # 23:59
|
||||
font_size: 46
|
||||
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]
|
||||
font_size: 14
|
||||
fg_color: "#ffffff"
|
||||
source: Clock
|
||||
format: "%x" # local date representation
|
||||
font_size: 14
|
||||
fg_color: "#ffffff"
|
||||
- type: Clock
|
||||
|
||||
# local day-of-week
|
||||
- type: Label
|
||||
rect: [20, 137, 200, 50]
|
||||
#format: "%a" # Tue
|
||||
format: "%A" # Tuesday
|
||||
font_size: 14
|
||||
fg_color: "#ffffff"
|
||||
source: Clock
|
||||
format: "%A" # Tuesday
|
||||
#format: "%a" # Tue
|
||||
|
||||
# alt clock 1
|
||||
- type: Clock
|
||||
- type: Label
|
||||
rect: [210, 90, 200, 50]
|
||||
timezone: "Asia/Tokyo" # change TZ1 here
|
||||
format: "%H:%M"
|
||||
font_size: 24
|
||||
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
|
||||
rect: [210, 60, 200, 50]
|
||||
font_size: 14
|
||||
fg_color: "#99BBAA"
|
||||
source: Static
|
||||
text: "Tokyo" # change TZ1 label here
|
||||
|
||||
# alt clock 2
|
||||
- type: Clock
|
||||
- type: Label
|
||||
rect: [210, 150, 200, 50]
|
||||
timezone: "America/Chicago" # change TZ2 here
|
||||
format: "%H:%M"
|
||||
font_size: 24
|
||||
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
|
||||
rect: [210, 120, 200, 50]
|
||||
font_size: 14
|
||||
fg_color: "#AA99BB"
|
||||
source: Static
|
||||
text: "Chicago" # change TZ2 label here
|
||||
|
||||
- type: Batteries
|
||||
# batteries
|
||||
- type: BatteryList
|
||||
rect: [0, 0, 400, 30]
|
||||
font_size: 14
|
||||
fg_color: "#99BBAA"
|
||||
fg_color_low: "#B06060"
|
||||
fg_color_charging: "#6080A0"
|
||||
num_devices: 9
|
||||
low_threshold: 20
|
||||
layout: Horizontal
|
||||
normal_fg_color: "#99BBAA"
|
||||
# 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?
|
||||
low_threshold: 20
|
||||
|
||||
# volume buttons
|
||||
- type: ExecButton
|
||||
- type: Button
|
||||
rect: [327, 52, 46, 32]
|
||||
font_size: 14
|
||||
fg_color: "#FFFFFF"
|
||||
bg_color: "#505050"
|
||||
text: "+"
|
||||
exec: [ "pactl", "set-sink-volume", "@DEFAULT_SINK@", "+5%" ]
|
||||
- type: ExecButton
|
||||
click_down:
|
||||
- type: Exec
|
||||
command: [ "pactl", "set-sink-volume", "@DEFAULT_SINK@", "+5%" ]
|
||||
- type: Button
|
||||
rect: [327, 116, 46, 32]
|
||||
font_size: 14
|
||||
fg_color: "#FFFFFF"
|
||||
bg_color: "#505050"
|
||||
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 anyhow::bail;
|
||||
use glam::{Quat, Vec3};
|
||||
use glam::Vec3;
|
||||
use rodio::{OutputStream, OutputStreamHandle};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use vulkano::format::Format;
|
||||
|
||||
use crate::{
|
||||
@@ -15,9 +17,6 @@ use crate::{
|
||||
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 fc: FontCache,
|
||||
pub session: AppSession,
|
||||
@@ -27,6 +26,7 @@ pub struct AppState {
|
||||
pub input_state: InputState,
|
||||
pub hid_provider: Box<dyn HidProvider>,
|
||||
pub audio: AudioOutput,
|
||||
pub screens: SmallVec<[ScreenMeta; 8]>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@@ -62,6 +62,7 @@ impl AppState {
|
||||
input_state: InputState::new(),
|
||||
hid_provider: crate::hid::initialize(),
|
||||
audio: AudioOutput::new(),
|
||||
screens: smallvec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -70,12 +71,6 @@ pub struct AppSession {
|
||||
pub config_root_path: PathBuf,
|
||||
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_shift: Vec3,
|
||||
pub color_alt: Vec3,
|
||||
@@ -91,10 +86,6 @@ impl AppSession {
|
||||
AppSession {
|
||||
config_root_path,
|
||||
config,
|
||||
primary_hand: 1,
|
||||
watch_hand: 0,
|
||||
watch_pos: WATCH_DEFAULT_POS,
|
||||
watch_rot: WATCH_DEFAULT_ROT,
|
||||
color_norm: Vec3 {
|
||||
x: 0.,
|
||||
y: 1.,
|
||||
@@ -145,3 +136,16 @@ impl AudioOutput {
|
||||
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