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

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