Files
wayvr/wlx-overlay-s/src/overlays/watch.rs
2025-12-17 19:23:44 +09:00

486 lines
20 KiB
Rust

use std::{
collections::HashMap,
rc::Rc,
time::{Duration, Instant},
};
use glam::{Affine3A, Quat, Vec3, Vec3A, vec3};
use idmap::DirectIdMap;
use wgui::{
components::button::ComponentButton,
event::{CallbackDataCommon, EventAlterables, EventCallback, StyleSetRequest},
i18n::Translation,
layout::WidgetID,
parser::Fetchable,
renderer_vk::text::custom_glyph::CustomGlyphData,
taffy,
widget::{EventResult, label::WidgetLabel, sprite::WidgetSprite},
};
use wlx_common::{
common::LeftRight,
windowing::{OverlayWindowState, Positioning},
};
use crate::{
backend::{
input::TrackedDeviceRole,
task::{OverlayTask, TaskType},
},
gui::{
panel::{GuiPanel, NewGuiPanelParams, OnCustomAttribFunc, button::BUTTON_EVENTS},
timer::GuiTimer,
},
overlays::edit::LongPressButtonState,
state::AppState,
windowing::{
OverlaySelector, Z_ORDER_WATCH,
backend::{OverlayEventData, OverlayMeta},
manager::MAX_OVERLAY_SETS,
window::{OverlayCategory, OverlayWindowConfig, OverlayWindowData},
},
};
pub const WATCH_NAME: &str = "watch";
const MAX_TOOLBOX_BUTTONS: usize = 16;
const MAX_DEVICES: usize = 9;
pub const WATCH_POS: Vec3 = vec3(-0.03, -0.01, 0.125);
pub const WATCH_ROT: Quat = Quat::from_xyzw(-0.707_106_6, 0.000_796_361_8, 0.707_106_6, 0.0);
struct OverlayButton {
button: Rc<ComponentButton>,
label: WidgetID,
sprite: WidgetID,
condensed: bool,
}
#[derive(Default)]
struct WatchState {
current_set: Option<usize>,
set_buttons: Vec<Rc<ComponentButton>>,
overlay_buttons: Vec<OverlayButton>,
overlay_metas: Vec<OverlayMeta>,
edit_mode_widgets: Vec<(WidgetID, bool)>,
edit_add_widget: WidgetID,
device_role_icons: DirectIdMap<TrackedDeviceRole, CustomGlyphData>,
overlay_cat_icons: DirectIdMap<OverlayCategory, CustomGlyphData>,
devices: Vec<(WidgetID, WidgetID)>,
num_sets: usize,
delete: LongPressButtonState,
}
#[allow(clippy::significant_drop_tightening)]
#[allow(clippy::too_many_lines)]
pub fn create_watch(app: &mut AppState) -> anyhow::Result<OverlayWindowConfig> {
let state = WatchState::default();
let on_custom_attrib: OnCustomAttribFunc = Box::new(move |layout, attribs, _app| {
for (name, kind) in &BUTTON_EVENTS {
let Some(action) = attribs.get_value(name) else {
continue;
};
let mut args = action.split_whitespace();
let Some(command) = args.next() else {
continue;
};
let callback: EventCallback<AppState, WatchState> = match command {
"::EditModeDeleteDown" => Box::new(move |_common, _data, _app, state| {
state.delete.pressed = Instant::now();
Ok(EventResult::Consumed)
}),
"::EditModeDeleteUp" => Box::new(move |_common, _data, app, state| {
if state.delete.pressed.elapsed() < Duration::from_secs(1) {
return Ok(EventResult::Consumed);
}
app.tasks
.enqueue(TaskType::Overlay(OverlayTask::DeleteActiveSet));
Ok(EventResult::Consumed)
}),
"::EditModeAddSet" => Box::new(move |_common, _data, app, _state| {
app.tasks.enqueue(TaskType::Overlay(OverlayTask::AddSet));
Ok(EventResult::Consumed)
}),
"::EditModeOverlayToggle" => {
let arg = args.next().unwrap_or_default();
let Ok(idx) = arg.parse::<usize>() else {
log::error!("{command} has invalid argument: \"{arg}\"");
return;
};
Box::new(move |_common, _data, app, state| {
let Some(overlay) = state.overlay_metas.get(idx) else {
log::error!("No overlay at index {idx}.");
return Ok(EventResult::Consumed);
};
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
OverlaySelector::Id(overlay.id),
Box::new(move |app, owc| {
if owc.active_state.is_none() {
owc.activate(app);
} else {
owc.deactivate();
}
}),
)));
Ok(EventResult::Consumed)
})
}
"::SingleSetOverlayToggle" => {
let arg = args.next().unwrap_or_default();
let Ok(idx) = arg.parse::<usize>() else {
log::error!("{command} has invalid argument: \"{arg}\"");
return;
};
Box::new(move |_common, _data, app, state| {
let Some(overlay) = state.overlay_metas.get(idx) else {
log::error!("No overlay at index {idx}.");
return Ok(EventResult::Consumed);
};
app.tasks
.enqueue(TaskType::Overlay(OverlayTask::SoftToggleOverlay(
OverlaySelector::Id(overlay.id),
)));
Ok(EventResult::Consumed)
})
}
_ => return,
};
let id = layout.add_event_listener(attribs.widget_id, *kind, callback);
log::debug!("Registered {action} on {:?} as {id:?}", attribs.widget_id);
}
});
let watch_xml = app
.session
.config
.single_set_mode
.then_some("gui/watch-noset.xml")
.unwrap_or("gui/watch.xml");
let mut panel = GuiPanel::new_from_template(
app,
watch_xml,
state,
NewGuiPanelParams {
on_custom_id: Some(Box::new(
move |id, widget, doc_params, layout, parser_state, state| {
if id.starts_with("norm_") {
state.edit_mode_widgets.push((widget, false));
} else if &*id == "edit_add" {
state.edit_add_widget = widget;
} else if id.starts_with("edit_") {
state.edit_mode_widgets.push((widget, true));
} else if &*id == "sets" {
let node = layout.state.nodes[widget];
let num_children = layout.state.tree.children(node).iter().len();
for idx in 0..MAX_OVERLAY_SETS {
if idx >= num_children {
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert("display".into(), (idx + 1).to_string().into());
params.insert("idx".into(), idx.to_string().into());
parser_state.instantiate_template(
doc_params, "Set", layout, widget, params,
)?;
}
let comp = parser_state
.fetch_component_as::<ComponentButton>(&format!("set_{idx}"))?;
state.set_buttons.push(comp);
}
} else if &*id == "toolbox" || &*id == "toolbox-condensed" {
for idx in 0..MAX_TOOLBOX_BUTTONS {
let id_str = format!("overlay_{idx}");
let button = if let Some(button) = parser_state
.fetch_component_as::<ComponentButton>(&id_str)
.ok()
{
button
} else {
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert("idx".into(), idx.to_string().into());
parser_state.instantiate_template(
doc_params, "Overlay", layout, widget, params,
)?;
parser_state.fetch_component_as::<ComponentButton>(&id_str)?
};
state.overlay_buttons.push(OverlayButton {
button,
label: parser_state
.get_widget_id(&format!("overlay_{idx}_label"))
.inspect_err(|e| log::warn!("{e:?}"))
.unwrap_or_default(),
sprite: parser_state
.get_widget_id(&format!("overlay_{idx}_sprite"))
.inspect_err(|e| log::warn!("{e:?}"))
.unwrap_or_default(),
condensed: id.ends_with("-condensed"),
});
}
} else if id.starts_with("overlay_") && id.ends_with("_sprite") {
// store device icons from xml
let id_n = id
.replace("overlay_", "")
.replace("_sprite", "")
.parse::<u64>()?;
let category = match id_n {
0 => OverlayCategory::Panel,
1 => OverlayCategory::Screen,
2 => OverlayCategory::Mirror,
3 => OverlayCategory::WayVR,
_ => return Ok(()), // not parsing the first 4 elems
};
let sprite = layout
.state
.widgets
.get_as::<WidgetSprite>(widget)
.ok_or_else(|| {
anyhow::anyhow!("{id} is expected to be a sprite, but it isn't.")
})?;
let src = sprite.get_content().ok_or_else(|| {
anyhow::anyhow!("{id} is expected to have a src, but it doesn't.")
})?;
state.overlay_cat_icons.insert(category, src);
} else if id.starts_with("dev_") && id.ends_with("_sprite") {
// store device icons from xml
let id_n = id
.replace("dev_", "")
.replace("_sprite", "")
.parse::<u64>()?;
let role = match id_n {
0 => TrackedDeviceRole::Hmd,
1 => TrackedDeviceRole::LeftHand,
2 => TrackedDeviceRole::RightHand,
3 => TrackedDeviceRole::Tracker,
_ => return Ok(()), // not parsing the first 4 elems
};
let sprite = layout
.state
.widgets
.get_as::<WidgetSprite>(widget)
.ok_or_else(|| {
anyhow::anyhow!("{id} is expected to be a sprite, but it isn't.")
})?;
let src = sprite.get_content().ok_or_else(|| {
anyhow::anyhow!("{id} is expected to have a src, but it doesn't.")
})?;
state.device_role_icons.insert(role, src);
} else if &*id == "devices" {
let node = layout.state.nodes[widget];
let num_children = layout.state.tree.children(node).iter().len();
for idx in 0..MAX_DEVICES {
if idx >= num_children {
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert("idx".into(), idx.to_string().into());
params.insert("src".into(), String::new().into());
parser_state.instantiate_template(
doc_params, "Device", layout, widget, params,
)?;
}
let div = parser_state.get_widget_id(&format!("dev_{idx}"))?;
let spr = parser_state.get_widget_id(&format!("dev_{idx}_sprite"))?;
state.devices.push((div, spr));
}
}
Ok(())
},
)),
on_custom_attrib: Some(on_custom_attrib),
..Default::default()
},
)?;
panel.on_notify = Some(Box::new(|panel, app, event_data| {
let mut alterables = EventAlterables::default();
let mut com = CallbackDataCommon {
alterables: &mut alterables,
state: &panel.layout.state,
};
match event_data {
OverlayEventData::ActiveSetChanged(current_set) => {
if let Some(old_set) = panel.state.current_set.take()
&& let Some(old_set) = panel.state.set_buttons.get_mut(old_set)
{
old_set.set_sticky_state(&mut com, false);
}
if let Some(new_set) = current_set
&& let Some(new_set) = panel.state.set_buttons.get_mut(new_set)
{
new_set.set_sticky_state(&mut com, true);
}
panel.state.current_set = current_set;
}
OverlayEventData::NumSetsChanged(num_sets) => {
panel.state.num_sets = num_sets;
for (i, comp) in panel.state.set_buttons.iter().enumerate() {
let rect_id = comp.get_rect();
let display = if i < num_sets {
taffy::Display::Flex
} else {
taffy::Display::None
};
com.alterables
.set_style(rect_id, StyleSetRequest::Display(display));
}
let display = if num_sets < 7 {
taffy::Display::Flex
} else {
taffy::Display::None
};
com.alterables.set_style(
panel.state.edit_add_widget,
StyleSetRequest::Display(display),
);
}
OverlayEventData::EditModeChanged(edit_mode) => {
for (w, e) in &panel.state.edit_mode_widgets {
let display = if *e == edit_mode {
taffy::Display::Flex
} else {
taffy::Display::None
};
com.alterables
.set_style(*w, StyleSetRequest::Display(display));
}
let display = if edit_mode && panel.state.num_sets < 7 {
taffy::Display::Flex
} else {
taffy::Display::None
};
com.alterables.set_style(
panel.state.edit_add_widget,
StyleSetRequest::Display(display),
);
}
OverlayEventData::OverlaysChanged(metas) => {
panel.state.overlay_metas = metas;
for (idx, btn) in panel.state.overlay_buttons.iter().enumerate() {
let display = if let Some(meta) = panel.state.overlay_metas.get(idx) {
let name = btn
.condensed
.then(|| condense_overlay_name(&meta.name))
.unwrap_or_else(|| sanitize_overlay_name(&meta.name));
if let Some(mut label) =
panel.layout.state.widgets.get_as::<WidgetLabel>(btn.label)
{
label.set_text(&mut com, Translation::from_raw_text_rc(name));
} else {
btn.button
.set_text(&mut com, Translation::from_raw_text_rc(name));
}
if let Some(mut sprite) = panel
.layout
.state
.widgets
.get_as::<WidgetSprite>(btn.sprite)
&& let Some(glyph) = panel.state.overlay_cat_icons.get(meta.category)
{
sprite.set_content(Some(glyph.clone()));
}
taffy::Display::Flex
} else {
taffy::Display::None
};
com.alterables
.set_style(btn.button.get_rect(), StyleSetRequest::Display(display));
}
}
OverlayEventData::DevicesChanged => {
for (i, (div, s)) in panel.state.devices.iter().enumerate() {
if let Some(dev) = app.input_state.devices.get(i)
&& let Some(glyph) = panel.state.device_role_icons.get(dev.role)
&& let Some(mut s) = panel.layout.state.widgets.get_as::<WidgetSprite>(*s)
{
s.set_content(Some(glyph.clone()));
com.alterables
.set_style(*div, StyleSetRequest::Display(taffy::Display::Flex));
} else {
com.alterables
.set_style(*div, StyleSetRequest::Display(taffy::Display::None));
}
}
}
}
panel.layout.process_alterables(alterables)?;
Ok(())
}));
panel
.timers
.push(GuiTimer::new(Duration::from_millis(100), 0));
let positioning = Positioning::FollowHand {
hand: LeftRight::Left,
lerp: 1.0,
};
panel.update_layout()?;
Ok(OverlayWindowConfig {
name: WATCH_NAME.into(),
z_order: Z_ORDER_WATCH,
default_state: OverlayWindowState {
interactable: true,
positioning,
transform: Affine3A::from_scale_rotation_translation(
Vec3::ONE * 0.115,
WATCH_ROT,
WATCH_POS,
),
..OverlayWindowState::default()
},
show_on_spawn: true,
global: true,
..OverlayWindowConfig::from_backend(Box::new(panel))
})
}
pub fn watch_fade<D>(app: &mut AppState, watch: &mut OverlayWindowData<D>) {
let Some(state) = watch.config.active_state.as_mut() else {
return;
};
let to_hmd = (state.transform.translation - app.input_state.hmd.translation).normalize();
let watch_normal = state.transform.transform_vector3a(Vec3A::NEG_Z).normalize();
let dot = to_hmd.dot(watch_normal);
state.alpha = (dot - app.session.config.watch_view_angle_min)
/ (app.session.config.watch_view_angle_max - app.session.config.watch_view_angle_min);
state.alpha += 0.1;
state.alpha = state.alpha.clamp(0., 1.);
}
fn sanitize_overlay_name(str: &str) -> Rc<str> {
str.replace("-wvr", "").into()
}
fn condense_overlay_name(str: &str) -> Rc<str> {
str.replace("DP-", "D")
.replace("HDMI-A-", "H")
.replace("WVR-wvr_", "W")
.replace("WVR-wvr", "W0")
.replace("Keyboard", "")
.into()
}