customizable watch
This commit is contained in:
@@ -2,6 +2,7 @@ use crate::config_io;
|
||||
use crate::config_io::get_conf_d_path;
|
||||
use crate::load_with_fallback;
|
||||
use crate::overlays::keyboard;
|
||||
use crate::overlays::watch::WatchConfig;
|
||||
use log::error;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -71,6 +72,11 @@ pub fn load_keyboard() -> keyboard::Layout {
|
||||
serde_yaml::from_str(&yaml_data).expect("Failed to parse keyboard.yaml")
|
||||
}
|
||||
|
||||
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_general() -> GeneralConfig {
|
||||
let mut yaml_data = String::new();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::bail;
|
||||
use glam::{Vec2, Vec3, Vec4};
|
||||
use vulkano::{
|
||||
command_buffer::CommandBufferUsage,
|
||||
@@ -28,12 +29,20 @@ struct Rect {
|
||||
}
|
||||
|
||||
// Parses a color from a HTML hex string
|
||||
pub fn color_parse(html_hex: &str) -> Vec3 {
|
||||
let mut color = Vec3::ZERO;
|
||||
color.x = u8::from_str_radix(&html_hex[1..3], 16).unwrap() as f32 / 255.;
|
||||
color.y = u8::from_str_radix(&html_hex[3..5], 16).unwrap() as f32 / 255.;
|
||||
color.z = u8::from_str_radix(&html_hex[5..7], 16).unwrap() as f32 / 255.;
|
||||
color
|
||||
pub fn color_parse(html_hex: &str) -> anyhow::Result<Vec3> {
|
||||
if html_hex.len() == 7 {
|
||||
if let (Ok(r), Ok(g), Ok(b)) = (
|
||||
u8::from_str_radix(&html_hex[1..3], 16),
|
||||
u8::from_str_radix(&html_hex[3..5], 16),
|
||||
u8::from_str_radix(&html_hex[5..7], 16),
|
||||
) {
|
||||
return Ok(Vec3::new(r as f32 / 255., g as f32 / 255., b as f32 / 255.));
|
||||
}
|
||||
}
|
||||
bail!(
|
||||
"Invalid color string: '{}', must be 7 characters long (e.g. #FF00FF)",
|
||||
&html_hex
|
||||
)
|
||||
}
|
||||
|
||||
pub struct CanvasBuilder<D, S> {
|
||||
@@ -625,7 +634,7 @@ impl<D, S> Control<D, S> {
|
||||
let set0 = canvas.pipeline_fg_glyph.uniform_sampler(
|
||||
0,
|
||||
ImageView::new_default(tex).unwrap(),
|
||||
Filter::Nearest,
|
||||
Filter::Linear,
|
||||
);
|
||||
let set1 = canvas.pipeline_fg_glyph.uniform_buffer(
|
||||
1,
|
||||
@@ -670,7 +679,7 @@ impl<D, S> Control<D, S> {
|
||||
let set0 = canvas.pipeline_fg_glyph.uniform_sampler(
|
||||
0,
|
||||
ImageView::new_default(tex).unwrap(),
|
||||
Filter::Nearest,
|
||||
Filter::Linear,
|
||||
);
|
||||
let set1 = canvas.pipeline_fg_glyph.uniform_buffer(
|
||||
1,
|
||||
|
||||
@@ -52,11 +52,11 @@ where
|
||||
data,
|
||||
);
|
||||
|
||||
canvas.bg_color = color_parse("#101010");
|
||||
canvas.bg_color = color_parse("#101010").unwrap();
|
||||
canvas.panel(0., 0., size.x, size.y);
|
||||
|
||||
canvas.font_size = 18;
|
||||
canvas.bg_color = color_parse("#202020");
|
||||
canvas.bg_color = color_parse("#202020").unwrap();
|
||||
|
||||
let unit_size = size.x / LAYOUT.row_size;
|
||||
let h = unit_size - 2. * BUTTON_PADDING;
|
||||
|
||||
@@ -201,7 +201,7 @@ impl ScreenPipeline {
|
||||
|
||||
let set0 = self
|
||||
.pipeline
|
||||
.uniform_sampler(0, mouse_view.clone(), Filter::Nearest);
|
||||
.uniform_sampler(0, mouse_view.clone(), Filter::Linear);
|
||||
|
||||
let pass = self.pipeline.create_pass(
|
||||
self.extentf,
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
use std::{sync::Arc, time::Instant};
|
||||
use std::{
|
||||
io::Read,
|
||||
process::{self, Stdio},
|
||||
sync::Arc,
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use chrono::Local;
|
||||
use glam::{vec2, Affine2};
|
||||
use chrono_tz::Tz;
|
||||
use glam::{vec2, Affine2, Vec3};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
backend::{
|
||||
@@ -9,116 +16,209 @@ use crate::{
|
||||
input::PointerMode,
|
||||
overlay::{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.,
|
||||
};
|
||||
|
||||
pub const WATCH_NAME: &str = "watch";
|
||||
|
||||
pub fn create_watch<O>(state: &AppState, screens: &[OverlayData<O>]) -> OverlayData<O>
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
let mut canvas = CanvasBuilder::new(400, 200, state.graphics.clone(), state.format, ());
|
||||
let config = load_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("");
|
||||
|
||||
// Background
|
||||
canvas.bg_color = color_parse("#353535");
|
||||
canvas.panel(0., 0., 400., 200.);
|
||||
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);
|
||||
|
||||
// Time display
|
||||
canvas.font_size = 46;
|
||||
let clock = canvas.label(19., 100., 200., 50., empty_str.clone());
|
||||
clock.on_update = Some(|control, _data, _app| {
|
||||
let date = Local::now();
|
||||
control.set_text(&format!("{}", &date.format("%H:%M")));
|
||||
});
|
||||
let tz: Option<Tz> = match timezone {
|
||||
Some(tz) => Some(tz.parse().unwrap_or_else(|_| {
|
||||
log::error!("Failed to parse timezone '{}'", &tz);
|
||||
canvas.fg_color = FALLBACK_COLOR;
|
||||
Tz::UTC
|
||||
})),
|
||||
None => None,
|
||||
};
|
||||
|
||||
canvas.font_size = 14;
|
||||
let date = canvas.label(20., 125., 200., 50., empty_str.clone());
|
||||
date.on_update = Some(|control, _data, _app| {
|
||||
let date = Local::now();
|
||||
control.set_text(&format!("{}", &date.format("%x")));
|
||||
});
|
||||
let label = canvas.label(x, y, w, h, empty_str.clone());
|
||||
label.state = Some(ElemState {
|
||||
clock: Some(ClockState {
|
||||
timezone: tz,
|
||||
format,
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
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 {
|
||||
exec: Some(ExecState {
|
||||
last_exec: Instant::now(),
|
||||
interval,
|
||||
exec,
|
||||
child: None,
|
||||
}),
|
||||
button: None,
|
||||
..Default::default()
|
||||
});
|
||||
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 {
|
||||
exec: Some(ExecState {
|
||||
last_exec: Instant::now(),
|
||||
interval: 0.,
|
||||
exec,
|
||||
child: None,
|
||||
}),
|
||||
button: Some(WatchButtonState {
|
||||
pressed_at: Instant::now(),
|
||||
mode: PointerMode::Left,
|
||||
overlay: None,
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
button.on_press = Some(exec_button);
|
||||
}
|
||||
WatchElement::OverlayList {
|
||||
rect,
|
||||
font_size,
|
||||
kbd_fg_color,
|
||||
kbd_bg_color,
|
||||
scr_fg_color,
|
||||
scr_bg_color,
|
||||
layout,
|
||||
} => {
|
||||
let num_buttons = screens.len() + 1;
|
||||
let mut button_x = rect[0];
|
||||
let mut button_y = rect[1];
|
||||
let (button_w, button_h) = match layout {
|
||||
ListLayout::Horizontal => (rect[2] / (num_buttons as f32), rect[3]),
|
||||
ListLayout::Vertical => (rect[2], rect[3] / (num_buttons as f32)),
|
||||
};
|
||||
|
||||
let day_of_week = canvas.label(20., 150., 200., 50., empty_str);
|
||||
day_of_week.on_update = Some(|control, _data, _app| {
|
||||
let date = Local::now();
|
||||
control.set_text(&format!("{}", &date.format("%A")));
|
||||
});
|
||||
canvas.bg_color = color_parse(&kbd_bg_color).unwrap_or(FALLBACK_COLOR);
|
||||
canvas.fg_color = color_parse(&kbd_fg_color).unwrap_or(FALLBACK_COLOR);
|
||||
canvas.font_size = font_size;
|
||||
|
||||
// Volume controls
|
||||
canvas.bg_color = color_parse("#222222");
|
||||
canvas.fg_color = color_parse("#AAAAAA");
|
||||
canvas.font_size = 14;
|
||||
let keyboard = canvas.button(
|
||||
button_x + 2.,
|
||||
button_y + 2.,
|
||||
button_w - 4.,
|
||||
button_h - 4.,
|
||||
KEYBOARD_NAME.into(),
|
||||
);
|
||||
keyboard.state = Some(ElemState {
|
||||
button: Some(WatchButtonState {
|
||||
pressed_at: Instant::now(),
|
||||
overlay: Some(OverlaySelector::Name(KEYBOARD_NAME.into())),
|
||||
mode: PointerMode::Left,
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
keyboard.on_press = Some(overlay_button_dn);
|
||||
keyboard.on_release = Some(overlay_button_up);
|
||||
|
||||
canvas.bg_color = color_parse("#303030");
|
||||
canvas.fg_color = color_parse("#353535");
|
||||
canvas.bg_color = color_parse(&scr_bg_color).unwrap_or(FALLBACK_COLOR);
|
||||
canvas.fg_color = color_parse(&scr_fg_color).unwrap_or(FALLBACK_COLOR);
|
||||
|
||||
let vol_up = canvas.button(327., 116., 46., 32., "+".into());
|
||||
vol_up.on_press = Some(|_control, _data, _app, _| {
|
||||
println!("Volume up!"); //TODO
|
||||
});
|
||||
for screen in screens.iter() {
|
||||
button_x += match layout {
|
||||
ListLayout::Horizontal => button_w,
|
||||
ListLayout::Vertical => 0.,
|
||||
};
|
||||
button_y += match layout {
|
||||
ListLayout::Horizontal => 0.,
|
||||
ListLayout::Vertical => button_h,
|
||||
};
|
||||
|
||||
let vol_dn = canvas.button(327., 52., 46., 32., "-".into());
|
||||
vol_dn.on_press = Some(|_control, _data, _app, _| {
|
||||
println!("Volume down!"); //TODO
|
||||
});
|
||||
let button = canvas.button(
|
||||
button_x + 2.,
|
||||
button_y + 2.,
|
||||
button_w - 4.,
|
||||
button_h - 4.,
|
||||
screen.state.name.clone(),
|
||||
);
|
||||
button.state = Some(ElemState {
|
||||
button: Some(WatchButtonState {
|
||||
pressed_at: Instant::now(),
|
||||
overlay: Some(OverlaySelector::Id(screen.state.id)),
|
||||
mode: PointerMode::Left,
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
canvas.bg_color = color_parse("#303030");
|
||||
canvas.fg_color = color_parse("#353535");
|
||||
|
||||
let settings = canvas.button(2., 162., 36., 36., "☰".into());
|
||||
settings.on_press = Some(|_control, _data, _app, _| {
|
||||
println!("Settings!"); //TODO
|
||||
});
|
||||
|
||||
canvas.fg_color = color_parse("#CCBBAA");
|
||||
canvas.bg_color = color_parse("#406050");
|
||||
// Bottom row
|
||||
let num_buttons = screens.len() + 1;
|
||||
let button_width = 360. / num_buttons as f32;
|
||||
let mut button_x = 40.;
|
||||
|
||||
let keyboard = canvas.button(
|
||||
button_x + 2.,
|
||||
162.,
|
||||
button_width - 4.,
|
||||
36.,
|
||||
KEYBOARD_NAME.into(),
|
||||
);
|
||||
keyboard.state = Some(WatchButtonState {
|
||||
pressed_at: Instant::now(),
|
||||
overlay: OverlaySelector::Name(KEYBOARD_NAME.into()),
|
||||
mode: PointerMode::Left,
|
||||
});
|
||||
|
||||
keyboard.on_press = Some(overlay_button_dn);
|
||||
keyboard.on_release = Some(overlay_button_up);
|
||||
button_x += button_width;
|
||||
|
||||
canvas.bg_color = color_parse("#405060");
|
||||
|
||||
for screen in screens.iter() {
|
||||
let button = canvas.button(
|
||||
button_x + 2.,
|
||||
162.,
|
||||
button_width - 4.,
|
||||
36.,
|
||||
screen.state.name.clone(),
|
||||
);
|
||||
button.state = Some(WatchButtonState {
|
||||
pressed_at: Instant::now(),
|
||||
overlay: OverlaySelector::Id(screen.state.id),
|
||||
mode: PointerMode::Left,
|
||||
});
|
||||
|
||||
button.on_press = Some(overlay_button_dn);
|
||||
button.on_release = Some(overlay_button_up);
|
||||
button_x += button_width;
|
||||
button.on_press = Some(overlay_button_dn);
|
||||
button.on_release = Some(overlay_button_up);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let interaction_transform =
|
||||
Affine2::from_translation(vec2(0.5, 0.5)) * Affine2::from_scale(vec2(1., -2.0));
|
||||
|
||||
@@ -142,84 +242,279 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ElemState {
|
||||
clock: Option<ClockState>,
|
||||
exec: Option<ExecState>,
|
||||
button: Option<WatchButtonState>,
|
||||
}
|
||||
|
||||
struct ClockState {
|
||||
timezone: Option<Tz>,
|
||||
format: Arc<str>,
|
||||
}
|
||||
|
||||
struct WatchButtonState {
|
||||
pressed_at: Instant,
|
||||
mode: PointerMode,
|
||||
overlay: OverlaySelector,
|
||||
overlay: Option<OverlaySelector>,
|
||||
}
|
||||
|
||||
struct ExecState {
|
||||
last_exec: Instant,
|
||||
interval: f32,
|
||||
exec: Vec<Arc<str>>,
|
||||
child: Option<process::Child>,
|
||||
}
|
||||
|
||||
fn exec_button(
|
||||
control: &mut Control<(), ElemState>,
|
||||
_: &mut (),
|
||||
_: &mut AppState,
|
||||
_mode: PointerMode,
|
||||
) {
|
||||
let state = control.state.as_mut().unwrap();
|
||||
let exec = state.exec.as_mut().unwrap();
|
||||
if let Some(child) = &mut exec.child {
|
||||
match child.try_wait() {
|
||||
Ok(Some(code)) => {
|
||||
if !code.success() {
|
||||
log::error!("Child process exited with code: {}", code);
|
||||
}
|
||||
exec.child = None;
|
||||
}
|
||||
Ok(None) => {
|
||||
log::warn!("Unable to launch child process: previous child not exited yet");
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
exec.child = None;
|
||||
log::error!("Error checking child process: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
let args = exec.exec.iter().map(|s| s.as_ref()).collect::<Vec<&str>>();
|
||||
match process::Command::new(args[0]).args(&args[1..]).spawn() {
|
||||
Ok(child) => {
|
||||
exec.child = Some(child);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to spawn process {:?}: {:?}", args, e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn exec_label_update(control: &mut Control<(), ElemState>, _: &mut (), _: &mut AppState) {
|
||||
let state = control.state.as_mut().unwrap();
|
||||
let exec = state.exec.as_mut().unwrap();
|
||||
|
||||
if let Some(mut child) = exec.child.take() {
|
||||
match child.try_wait() {
|
||||
Ok(Some(code)) => {
|
||||
if !code.success() {
|
||||
log::error!("Child process exited with code: {}", code);
|
||||
} else {
|
||||
if let Some(mut stdout) = child.stdout.take() {
|
||||
let mut buf = String::new();
|
||||
if let Ok(_) = stdout.read_to_string(&mut buf) {
|
||||
control.set_text(&buf);
|
||||
} else {
|
||||
log::error!("Failed to read stdout for child process");
|
||||
return;
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
log::error!("No stdout for child process");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
exec.child = Some(child);
|
||||
// not exited yet
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
exec.child = None;
|
||||
log::error!("Error checking child process: {:?}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if Instant::now()
|
||||
.saturating_duration_since(exec.last_exec)
|
||||
.as_secs_f32()
|
||||
> exec.interval
|
||||
{
|
||||
exec.last_exec = Instant::now();
|
||||
let args = exec.exec.iter().map(|s| s.as_ref()).collect::<Vec<&str>>();
|
||||
|
||||
match process::Command::new(args[0])
|
||||
.args(&args[1..])
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
{
|
||||
Ok(child) => {
|
||||
exec.child = Some(child);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to spawn process {:?}: {:?}", args, e);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn clock_update(control: &mut Control<(), ElemState>, _: &mut (), _: &mut AppState) {
|
||||
let state = control.state.as_mut().unwrap();
|
||||
let clock = state.clock.as_mut().unwrap();
|
||||
|
||||
let fmt = clock.format.clone();
|
||||
|
||||
if let Some(tz) = clock.timezone {
|
||||
let date = Local::now().with_timezone(&tz);
|
||||
control.set_text(&format!("{}", &date.format(&fmt)));
|
||||
} else {
|
||||
let date = Local::now();
|
||||
control.set_text(&format!("{}", &date.format(&fmt)));
|
||||
}
|
||||
}
|
||||
|
||||
fn overlay_button_dn(
|
||||
control: &mut Control<(), WatchButtonState>,
|
||||
control: &mut Control<(), ElemState>,
|
||||
_: &mut (),
|
||||
_: &mut AppState,
|
||||
mode: PointerMode,
|
||||
) {
|
||||
if let Some(state) = control.state.as_mut() {
|
||||
state.pressed_at = Instant::now();
|
||||
state.mode = mode;
|
||||
let btn = control.state.as_mut().unwrap().button.as_mut().unwrap();
|
||||
btn.pressed_at = Instant::now();
|
||||
btn.mode = mode;
|
||||
}
|
||||
|
||||
fn overlay_button_up(control: &mut Control<(), ElemState>, _: &mut (), app: &mut AppState) {
|
||||
let btn = control.state.as_mut().unwrap().button.as_mut().unwrap();
|
||||
let selector = btn.overlay.as_ref().unwrap().clone();
|
||||
if Instant::now()
|
||||
.saturating_duration_since(btn.pressed_at)
|
||||
.as_millis()
|
||||
< 2000
|
||||
{
|
||||
match btn.mode {
|
||||
PointerMode::Left => {
|
||||
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);
|
||||
}
|
||||
}),
|
||||
));
|
||||
}
|
||||
PointerMode::Right => {
|
||||
app.tasks.enqueue(TaskType::Overlay(
|
||||
selector,
|
||||
Box::new(|app, o| {
|
||||
o.recenter = !o.recenter;
|
||||
o.grabbable = o.recenter;
|
||||
o.show_hide = o.recenter;
|
||||
if !o.recenter {
|
||||
app.tasks.enqueue(TaskType::Toast(Toast::new(
|
||||
format!("{} is now locked in place!", o.name).into(),
|
||||
"Right-click again to toggle.".into(),
|
||||
)))
|
||||
}
|
||||
}),
|
||||
));
|
||||
}
|
||||
PointerMode::Middle => {
|
||||
app.tasks.enqueue(TaskType::Overlay(
|
||||
selector,
|
||||
Box::new(|app, o| {
|
||||
o.interactable = !o.interactable;
|
||||
if !o.interactable {
|
||||
app.tasks.enqueue(TaskType::Toast(Toast::new(
|
||||
format!("{} is now non-interactable!", o.name).into(),
|
||||
"Middle-click again to toggle.".into(),
|
||||
)))
|
||||
}
|
||||
}),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
app.tasks.enqueue(TaskType::Overlay(
|
||||
selector,
|
||||
Box::new(|app, o| {
|
||||
o.reset(app, true);
|
||||
}),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn overlay_button_up(control: &mut Control<(), WatchButtonState>, _: &mut (), app: &mut AppState) {
|
||||
if let Some(state) = control.state.as_ref() {
|
||||
let selector = state.overlay.clone();
|
||||
if Instant::now()
|
||||
.saturating_duration_since(state.pressed_at)
|
||||
.as_millis()
|
||||
< 2000
|
||||
{
|
||||
match state.mode {
|
||||
PointerMode::Left => {
|
||||
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);
|
||||
}
|
||||
}),
|
||||
));
|
||||
}
|
||||
PointerMode::Right => {
|
||||
app.tasks.enqueue(TaskType::Overlay(
|
||||
selector,
|
||||
Box::new(|app, o| {
|
||||
o.recenter = !o.recenter;
|
||||
o.grabbable = o.recenter;
|
||||
o.show_hide = o.recenter;
|
||||
if !o.recenter {
|
||||
app.tasks.enqueue(TaskType::Toast(Toast::new(
|
||||
format!("{} is now locked in place!", o.name).into(),
|
||||
"Right-click again to toggle.".into(),
|
||||
)))
|
||||
}
|
||||
}),
|
||||
));
|
||||
}
|
||||
PointerMode::Middle => {
|
||||
app.tasks.enqueue(TaskType::Overlay(
|
||||
selector,
|
||||
Box::new(|app, o| {
|
||||
o.interactable = !o.interactable;
|
||||
if !o.interactable {
|
||||
app.tasks.enqueue(TaskType::Toast(Toast::new(
|
||||
format!("{} is now non-interactable!", o.name).into(),
|
||||
"Middle-click again to toggle.".into(),
|
||||
)))
|
||||
}
|
||||
}),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
app.tasks.enqueue(TaskType::Overlay(
|
||||
selector,
|
||||
Box::new(|app, o| {
|
||||
o.reset(app, true);
|
||||
}),
|
||||
));
|
||||
}
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
pub struct WatchConfig {
|
||||
watch_hand: LeftRight,
|
||||
watch_size: [u32; 2],
|
||||
watch_elements: Vec<WatchElement>,
|
||||
}
|
||||
|
||||
#[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>,
|
||||
},
|
||||
OverlayList {
|
||||
rect: [f32; 4],
|
||||
font_size: isize,
|
||||
kbd_fg_color: Arc<str>,
|
||||
kbd_bg_color: Arc<str>,
|
||||
scr_fg_color: Arc<str>,
|
||||
scr_bg_color: Arc<str>,
|
||||
layout: ListLayout,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
enum ListLayout {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
enum LeftRight {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
115
src/res/watch.yaml
Normal file
115
src/res/watch.yaml
Normal file
@@ -0,0 +1,115 @@
|
||||
# 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
|
||||
# - font_size:
|
||||
#
|
||||
# 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
|
||||
|
||||
watch_hand: Left
|
||||
|
||||
watch_offset: []
|
||||
|
||||
watch_rotation: []
|
||||
|
||||
watch_size: [400, 200]
|
||||
|
||||
watch_elements:
|
||||
# background panel
|
||||
- type: Panel
|
||||
rect: [0, 0, 400, 200]
|
||||
bg_color: "#353535"
|
||||
|
||||
# bottom row, of keyboard + overlays
|
||||
- type: OverlayList
|
||||
rect: [0, 160, 400, 40]
|
||||
font_size: 14
|
||||
kbd_fg_color: "#FFFFFF"
|
||||
kbd_bg_color: "#406050"
|
||||
scr_fg_color: "#FFFFFF"
|
||||
scr_bg_color: "#405060"
|
||||
layout: Horizontal
|
||||
|
||||
# main clock with date and day-of-week
|
||||
- type: Clock
|
||||
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
|
||||
rect: [20, 117, 200, 20]
|
||||
format: "%x" # local date representation
|
||||
font_size: 14
|
||||
fg_color: "#ffffff"
|
||||
- type: Clock
|
||||
rect: [20, 137, 200, 50]
|
||||
#format: "%a" # Tue
|
||||
format: "%A" # Tuesday
|
||||
font_size: 14
|
||||
fg_color: "#ffffff"
|
||||
|
||||
# alt clock 1
|
||||
- type: Clock
|
||||
rect: [210, 90, 200, 50]
|
||||
timezone: "Asia/Tokyo" # change TZ1 here
|
||||
format: "%H:%M"
|
||||
font_size: 24
|
||||
fg_color: "#99BBAA"
|
||||
- type: Label
|
||||
rect: [210, 60, 200, 50]
|
||||
font_size: 14
|
||||
fg_color: "#99BBAA"
|
||||
text: "Tokyo" # change TZ1 label here
|
||||
|
||||
# alt clock 2
|
||||
- type: Clock
|
||||
rect: [210, 150, 200, 50]
|
||||
timezone: "America/Chicago" # change TZ2 here
|
||||
format: "%H:%M"
|
||||
font_size: 24
|
||||
fg_color: "#AA99BB"
|
||||
- type: Label
|
||||
rect: [210, 120, 200, 50]
|
||||
font_size: 14
|
||||
fg_color: "#AA99BB"
|
||||
text: "Chicago" # change TZ2 label here
|
||||
|
||||
# sample
|
||||
- type: ExecLabel
|
||||
rect: [50, 20, 200, 50]
|
||||
font_size: 14
|
||||
fg_color: "#FFFFFF"
|
||||
exec: ["echo", "customize me! see watch.yaml"]
|
||||
interval: 0 # seconds
|
||||
|
||||
# volume buttons
|
||||
- type: ExecButton
|
||||
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
|
||||
rect: [327, 116, 46, 32]
|
||||
font_size: 14
|
||||
fg_color: "#FFFFFF"
|
||||
bg_color: "#505050"
|
||||
text: "-"
|
||||
exec: [ "pactl", "set-sink-volume", "@DEFAULT_SINK@", "-5%" ]
|
||||
Reference in New Issue
Block a user