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, label: WidgetID, sprite: WidgetID, condensed: bool, } #[derive(Default)] struct WatchState { current_set: Option, set_buttons: Vec>, overlay_buttons: Vec, overlay_metas: Vec, edit_mode_widgets: Vec<(WidgetID, bool)>, edit_add_widget: WidgetID, device_role_icons: DirectIdMap, overlay_cat_icons: DirectIdMap, 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 { 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 = 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::() 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::() 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> = 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::(&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::(&id_str) .ok() { button } else { let mut params: HashMap, Rc> = 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::(&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::()?; 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::(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::()?; 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::(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> = 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::(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::(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::(*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(app: &mut AppState, watch: &mut OverlayWindowData) { 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.replace("-wvr", "").into() } fn condense_overlay_name(str: &str) -> Rc { str.replace("DP-", "D") .replace("HDMI-A-", "H") .replace("WVR-wvr_", "W") .replace("WVR-wvr", "W0") .replace("Keyboard", "") .into() }