use std::{borrow::Cow, collections::HashMap, rc::Rc}; use wgui::{ assets::AssetPath, components::button::{ButtonClickEvent, ComponentButton}, globals::WguiGlobals, i18n::Translation, layout::{Layout, WidgetID}, log::LogErr, parser::{Fetchable, ParseDocumentParams, ParserState}, task::Tasks, widget::label::WidgetLabel, windowing::context_menu::{self, TickResult}, }; use wlx_common::{ config_io, openxr_actions::{OneOrMany, OpenXrInputAction, OpenXrInputProfile, load_xr_input_profiles}, }; use crate::{ frontend::{FrontendTask, FrontendTasks}, tab::settings::horiz_cell, util::{ openxr_bindings_schema::{ClickType, Component, IdentifierType, ParsedOpenXrInputPath, Profile, Side}, popup_manager::{MountPopupOnceParams, PopupHolder}, }, views::{ViewTrait, ViewUpdateParams}, }; #[derive(Clone)] enum Task { Save, Cancel, OpenContextMenu(glam::Vec2, Vec), } pub struct Params<'a> { pub globals: WguiGlobals, pub layout: &'a mut Layout, pub parent_id: WidgetID, pub profile_id: Rc, pub profile: Rc, pub close_callback: Box, } pub struct View { parser_state: ParserState, tasks: Tasks, list_parent: WidgetID, globals: WguiGlobals, profiles: Vec, cur_profile_idx: usize, context_menu: context_menu::ContextMenu, schema: Rc, close_callback: Option>, } impl ViewTrait for View { fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> { for task in self.tasks.drain() { match task { Task::Save => { let content = serde_json::to_string_pretty(&self.profiles) .map_err(|e| anyhow::anyhow!("Failed to serialize profiles: {e}"))?; config_io::save("openxr_actions.json5", &content) .map_err(|e| anyhow::anyhow!("Failed to save bindings: {e}"))?; if let Some(close_callback) = self.close_callback.take() { close_callback(); } } Task::Cancel => { if let Some(close_callback) = self.close_callback.take() { close_callback(); } } Task::OpenContextMenu(position, cells) => { self.context_menu.open(context_menu::OpenParams { on_custom_attribs: None, position, blueprint: context_menu::Blueprint::Cells(cells), }); } } } // Dropdown handling if let TickResult::Action(name) = self.context_menu.tick(&mut par.layout, &mut self.parser_state)? && let (Some(action), Some(_), Some(action_name), Some(side), Some(value)) = { let mut s = name.splitn(5, ';'); (s.next(), s.next(), s.next(), s.next(), s.next()) } { let side = side.to_lowercase(); let value = value.to_lowercase(); log::warn!("{action_name}"); let mut cur_profile = &mut self.profiles[self.cur_profile_idx]; let action_mut = get_action_mut(&mut cur_profile, action_name); let side_mut = if side == "right" { &mut action_mut.right } else { &mut action_mut.left }; match action { "clear" => { *side_mut = None; } "subpath" => { reconstruct_path(&self.schema, side_mut, &side, Some(value.as_str()), None); } "comp" => { reconstruct_path(&self.schema, side_mut, &side, None, Some(value.as_str())); } "click" => match value.as_str() { "triple" => { action_mut.triple_click = Some(true); action_mut.double_click = None; } "double" => { action_mut.triple_click = None; action_mut.double_click = Some(true); } _ => { action_mut.triple_click = None; action_mut.double_click = None; } }, _ => log::warn!("Unknown action {action}"), } self.refresh(par.layout)?; } Ok(()) } } impl View { pub fn new(params: Params) -> anyhow::Result { let doc_params = &ParseDocumentParams { globals: params.globals.clone(), path: AssetPath::BuiltIn("gui/view/bindings.xml"), extra: Default::default(), }; let mut profiles = load_xr_input_profiles(); let cur_profile_idx = profiles .iter() .position(|i| i.profile.as_str() == &*params.profile_id) .unwrap_or_else(|| { let idx = profiles.len(); profiles.push(OpenXrInputProfile { profile: params.profile_id.to_string(), ..Default::default() }); idx }); let parser_state = wgui::parser::parse_from_assets(doc_params, params.layout, params.parent_id)?; let list_parent = parser_state.fetch_widget(¶ms.layout.state, "list_parent")?.id; let tasks = Tasks::new(); { let mut title_label = parser_state.fetch_widget_as::(¶ms.layout.state, "controller_type")?; title_label.set_text_simple( &mut params.globals.get(), Translation::from_raw_text_rc(params.profile.title.clone()), ); } tasks.handle_button( &parser_state.fetch_component_as::("btn_save")?, Task::Save, ); tasks.handle_button( &parser_state.fetch_component_as::("btn_cancel")?, Task::Cancel, ); let mut me = Self { parser_state, tasks, list_parent, globals: params.globals.clone(), profiles, cur_profile_idx, context_menu: context_menu::ContextMenu::default(), schema: params.profile, close_callback: Some(params.close_callback), }; me.refresh(params.layout)?; Ok(me) } fn refresh(&mut self, layout: &mut Layout) -> anyhow::Result<()> { let action_names = [ "click", "grab", "alt_click", "show_hide", "toggle_dashboard", "space_drag", "space_rotate", "space_reset", "click_modifier_right", "click_modifier_middle", "move_mouse", "scroll", ]; let mut mp = MacroParams { parser_state: &mut self.parser_state, doc_params: &ParseDocumentParams { globals: self.globals.clone(), path: AssetPath::BuiltIn("gui/view/bindings.xml"), extra: Default::default(), }, layout, tasks: self.tasks.clone(), idx: 0, }; mp.layout.remove_children(self.list_parent); for action in action_names { let current = get_action_mut(&mut self.profiles[self.cur_profile_idx], action); input_controls_for_action(&mut mp, self.list_parent, action.into(), &self.schema, current)?; } Ok(()) } } fn get_action_mut<'a>(profile: &'a mut OpenXrInputProfile, action_name: &str) -> &'a mut OpenXrInputAction { let action = match action_name { "click" => &mut profile.click, "grab" => &mut profile.grab, "alt_click" => &mut profile.alt_click, "show_hide" => &mut profile.show_hide, "toggle_dashboard" => &mut profile.toggle_dashboard, "space_drag" => &mut profile.space_drag, "space_rotate" => &mut profile.space_rotate, "space_reset" => &mut profile.space_reset, "click_modifier_right" => &mut profile.click_modifier_right, "click_modifier_middle" => &mut profile.click_modifier_middle, "move_mouse" => &mut profile.move_mouse, "scroll" => &mut profile.scroll, _ => panic!("unknown action_name: {action_name}"), }; action.get_or_insert_with(OpenXrInputAction::default) } pub fn mount_popup( frontend_tasks: FrontendTasks, globals: WguiGlobals, popup: PopupHolder, profile_id: Rc, profile: Rc, ) { frontend_tasks .clone() .push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new( Translation::from_translation_key("APP_SETTINGS.INPUT_PROFILES"), Box::new(move |data| { let close_callback = popup.get_close_callback(data.layout); let view = View::new(Params { globals: globals.clone(), layout: data.layout, parent_id: data.id_content, profile_id, profile, close_callback, })?; popup.set_view(data.handle, view, None); Ok(popup.get_close_callback(data.layout)) }), ))); } struct MacroParams<'a> { pub layout: &'a mut Layout, pub parser_state: &'a mut ParserState, pub doc_params: &'a ParseDocumentParams<'a>, pub tasks: Tasks, pub idx: usize, } fn input_controls_for_action( mp: &mut MacroParams, parent: WidgetID, action: Rc, profile: &Profile, current: &mut OpenXrInputAction, ) -> anyhow::Result<()> { let id = mp.idx.to_string(); mp.idx += 1; let mut params: HashMap, Rc> = HashMap::new(); params.insert(Rc::from("id"), Rc::from(id.as_ref())); params.insert( Rc::from("translation"), Rc::from(format!( "APP_SETTINGS.BINDINGS.ACTION.{}", action.as_ref().to_uppercase() )), ); mp.parser_state .instantiate_template(mp.doc_params, "ActionRow", mp.layout, parent, params)?; let parent = mp.parser_state.get_widget_id(&id)?; let click_type = if current.triple_click.unwrap_or_default() { ClickType::Triple } else if current.double_click.unwrap_or_default() { ClickType::Double } else { ClickType::Any }; let current_left = current.left.as_ref().map(|x| match x { OneOrMany::One(s) => s.as_str(), OneOrMany::Many(s) => s.first().unwrap().as_str(), // safe }); input_controls_for_hand( mp, parent, current_left, Side::Left, action.clone(), click_type, profile, )?; let current_right = current.right.as_ref().map(|x| match x { OneOrMany::One(s) => s.as_str(), OneOrMany::Many(s) => s.first().unwrap().as_str(), // safe }); input_controls_for_hand(mp, parent, current_right, Side::Right, action, click_type, profile) } fn input_controls_for_hand( mp: &mut MacroParams, parent: WidgetID, current: Option<&str>, side: Side, action: Rc, click_type: ClickType, profile: &Profile, ) -> anyhow::Result<()> { let subaction_path = match side { Side::Left => "/user/hand/left", Side::Right => "/user/hand/right", }; if !profile.subaction_paths.iter().any(|p| p == subaction_path) { return Ok(()); // skip } let current = current .map(|cur| ParsedOpenXrInputPath::try_from(cur).log_warn(cur).ok()) .flatten(); let parent = horiz_cell(mp.layout, parent)?; let available_components = current .as_ref() .map(|par| profile.subpaths.get(&par.to_subpath())) .flatten() .map(|subp| subp.get_effective_components()) .unwrap_or_default(); let available_subpaths = profile .subpaths .iter() .filter(|(_, path)| path.side.is_none_or(|s| s == side)) .filter_map(|(key, _)| { key .strip_prefix("/input/") .map(|ident| IdentifierType::try_from(ident).ok()) .flatten() }) .collect::>(); subpath_dropdown( mp, parent, action.clone(), side, available_subpaths, current.as_ref().map(|x| x.identifier), )?; if !component_dropdown( mp, parent, action.clone(), side, available_components, current.as_ref().map(|x| x.component), )? { return Ok(()); } clicks_dropdown(mp, parent, action, click_type)?; Ok(()) } fn subpath_dropdown( mp: &mut MacroParams, parent: WidgetID, action: Rc, side: Side, available: Rc<[IdentifierType]>, current: Option, ) -> anyhow::Result<()> { let id = mp.idx.to_string(); mp.idx += 1; let mut params: HashMap, Rc> = HashMap::new(); params.insert(Rc::from("id"), Rc::from(id.as_ref())); params.insert( Rc::from("translation"), Rc::from(format!("APP_SETTINGS.BINDINGS.{}", side.as_ref().to_uppercase())), ); params.insert(Rc::from("tooltip"), Rc::from("APP_SETTINGS.BINDINGS.SUBPATH")); mp.parser_state .instantiate_template(mp.doc_params, "DropdownButton", mp.layout, parent, params)?; let title = current .map(|c| c.translation()) .unwrap_or_else(|| Translation::from_translation_key("APP_SETTINGS.OPTION.NONE")); { let mut label = mp .parser_state .fetch_widget_as::(&mp.layout.state, &format!("{id}_value"))?; label.set_text_simple(&mut mp.layout.state.globals.get(), title); } let btn = mp.parser_state.fetch_component_as::(&id)?; btn.on_click(Rc::new({ let available = available.clone(); let tasks = mp.tasks.clone(); move |_common, e: ButtonClickEvent| { let side = side.as_ref(); let mut cells = available .iter() .map(|item| { let value = item.as_ref(); let title = item.translation(); context_menu::Cell { action_name: Some(format!("subpath;{id};{action};{side};{value}").into()), title, tooltip: None, attribs: vec![], } }) .collect::>(); cells.insert( 0, context_menu::Cell { action_name: Some(format!("clear;{id};{action};{side};-").into()), title: Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"), tooltip: None, attribs: vec![], }, ); tasks.push(Task::OpenContextMenu(e.mouse_pos_absolute.unwrap_or_default(), cells)); Ok(()) } })); Ok(()) } fn component_dropdown( mp: &mut MacroParams, parent: WidgetID, action: Rc, side: Side, available: Rc<[Component]>, current: Option, ) -> anyhow::Result { if available.is_empty() { return Ok(false); } let id = mp.idx.to_string(); mp.idx += 1; let mut params: HashMap, Rc> = HashMap::new(); params.insert(Rc::from("id"), Rc::from(id.as_ref())); params.insert(Rc::from("text"), Rc::from("・")); params.insert(Rc::from("tooltip"), Rc::from("APP_SETTINGS.BINDINGS.COMPONENT")); mp.parser_state .instantiate_template(mp.doc_params, "DropdownButton", mp.layout, parent, params)?; let title = current .map(|c| c.translation()) .unwrap_or_else(|| Translation::from_raw_text_rc(Default::default())); { let mut label = mp .parser_state .fetch_widget_as::(&mp.layout.state, &format!("{id}_value"))?; label.set_text_simple(&mut mp.layout.state.globals.get(), title); } let btn = mp.parser_state.fetch_component_as::(&id)?; btn.on_click(Rc::new({ let tasks = mp.tasks.clone(); let available = available.clone(); move |_common, e: ButtonClickEvent| { tasks.push(Task::OpenContextMenu( e.mouse_pos_absolute.unwrap_or_default(), available .iter() .filter_map(|item| { let value = item.as_ref(); let title = item.translation(); let side = side.as_ref(); Some(context_menu::Cell { action_name: Some(format!("comp;{id};{action};{side};{value}").into()), title, tooltip: None, attribs: vec![], }) }) .collect(), )); Ok(()) } })); Ok(true) } fn clicks_dropdown(mp: &mut MacroParams, parent: WidgetID, action: Rc, current: ClickType) -> anyhow::Result<()> { let id = mp.idx.to_string(); mp.idx += 1; let mut params: HashMap, Rc> = HashMap::new(); params.insert(Rc::from("id"), Rc::from(id.as_ref())); params.insert(Rc::from("text"), Rc::from("・")); params.insert(Rc::from("tooltip"), Rc::from("APP_SETTINGS.BINDINGS.CLICK.TYPE")); mp.parser_state .instantiate_template(mp.doc_params, "DropdownButton", mp.layout, parent, params)?; let title = current.translation(); { let mut label = mp .parser_state .fetch_widget_as::(&mp.layout.state, &format!("{id}_value"))?; label.set_text_simple(&mut mp.layout.state.globals.get(), title); } let btn = mp.parser_state.fetch_component_as::(&id)?; btn.on_click(Rc::new({ let tasks = mp.tasks.clone(); move |_common, e: ButtonClickEvent| { tasks.push(Task::OpenContextMenu( e.mouse_pos_absolute.unwrap_or_default(), [ClickType::Any, ClickType::Double, ClickType::Triple] .iter() .filter_map(|item| { let value = item.as_ref(); let title = item.translation(); Some(context_menu::Cell { action_name: Some(format!("click;{id};{action};-;{value}").into()), title, tooltip: None, attribs: vec![], }) }) .collect(), )); Ok(()) } })); Ok(()) } fn reconstruct_path( schema: &Profile, side_mut: &mut Option>, side: &str, subpath: Option<&str>, comp: Option<&str>, ) { if side_mut.is_none() { let Some(subpath) = subpath else { return; }; if let Some(comp) = comp { *side_mut = Some(OneOrMany::One(format!("/user/hand/{side}/input/{subpath}/{comp}"))); } else { let key = format!("/input/{subpath}"); let Some(schema_subpath) = schema.subpaths.get(&key) else { return; }; let comps = schema_subpath.get_effective_components(); let comp = comps.first().unwrap().as_ref(); // safe *side_mut = Some(OneOrMany::One(format!("/user/hand/{side}/input/{subpath}/{comp}"))); } } else { let first = match side_mut.as_ref().unwrap() { OneOrMany::One(x) => x.as_str(), OneOrMany::Many(x) => x.first().unwrap().as_str(), }; let Ok(parsed) = ParsedOpenXrInputPath::try_from(first) else { return; }; let new_subpath = subpath.map_or_else(|| Cow::Owned(parsed.identifier.as_ref().to_lowercase()), Cow::Borrowed); let mut parsed_compo = parsed.component; let key = format!("/input/{new_subpath}"); let Some(schema_subpath) = schema.subpaths.get(&key) else { return; }; let effective_compo = schema_subpath.get_effective_components(); if !effective_compo.contains(&parsed_compo) { parsed_compo = *effective_compo.first().unwrap(); } let new_comp = comp.map_or_else(|| Cow::Owned(parsed_compo.as_ref().to_lowercase()), Cow::Borrowed); *side_mut = Some(OneOrMany::One(format!( "/user/hand/{side}/input/{new_subpath}/{new_comp}" ))); } }