modular ui rework

This commit is contained in:
galister
2024-02-25 19:27:48 +01:00
parent b93ddfce5b
commit b045f46b12
20 changed files with 2161 additions and 1049 deletions

View File

@@ -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 {

View File

@@ -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) => {

View File

@@ -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(())
}

View File

@@ -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);
}
},
}
}

View File

@@ -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;

View File

@@ -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
}
}
}

View File

@@ -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,
)
}

View File

@@ -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 {

View File

@@ -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
View 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
View 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
View 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
View 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))
}

View File

@@ -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
}

View File

@@ -1,3 +1,4 @@
pub mod custom;
pub mod keyboard;
#[cfg(feature = "wayland")]
pub mod mirror;

View File

@@ -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);

View File

@@ -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
View 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

View File

@@ -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%" ]

View File

@@ -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,
}