modular ui rework
This commit is contained in:
@@ -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
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user