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