pub mod app; pub mod hex; use app::App; use color_eyre::eyre::Result; use glam::Quat; use hex::{HEX_CENTER, HEX_DIRECTION_VECTORS}; use manifest_dir_macros::directory_relative_path; use protostar::xdg::{get_desktop_files, parse_desktop_file, DesktopFile}; use serde::{Deserialize, Serialize}; use stardust_xr_fusion::{ client::Client, core::values::{ color::{color_space::LinearRgb, rgba_linear, Rgba}, ResourceID, }, drawable::{MaterialParameter, Model, ModelPartAspect}, node::{NodeError, NodeType}, root::{ClientState, FrameInfo, RootAspect, RootHandler}, spatial::{Spatial, SpatialAspect, Transform}, }; use stardust_xr_molecules::{ button::{Button, ButtonSettings}, Grabbable, GrabbableSettings, PointerMode, }; use std::{f32::consts::PI, sync::Arc, time::Duration}; const APP_SIZE: f32 = 0.06; const PADDING: f32 = 0.005; const MODEL_SCALE: f32 = 0.03; const ACTIVATION_DISTANCE: f32 = 0.05; const DEFAULT_HEX_COLOR: Rgba = rgba_linear!(0.211, 0.937, 0.588, 1.0); const BTN_SELECTED_COLOR: Rgba = rgba_linear!(0.0, 1.0, 0.0, 1.0); const BTN_COLOR: Rgba = rgba_linear!(1.0, 1.0, 0.0, 1.0); #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { color_eyre::install().unwrap(); tracing_subscriber::fmt() .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) .pretty() .init(); let (client, event_loop) = Client::connect_with_async_loop().await?; client .set_base_prefixes(&[directory_relative_path!("../res")]) .unwrap(); let _root = client .get_root() .alias() .wrap(AppHexGrid::new(&client).await)?; tokio::select! { _ = tokio::signal::ctrl_c() => (), e = event_loop => e??, } Ok(()) } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct State { unfurled: bool, } struct AppHexGrid { movable_root: Spatial, apps: Vec, button: CenterButton, state: State, } impl AppHexGrid { async fn new(client: &Arc) -> Self { let state = client.get_state().data().unwrap_or_default(); let movable_root = Spatial::create(client.get_root(), Transform::identity(), false).unwrap(); let button = CenterButton::new(client, &client.get_state()).unwrap(); tokio::time::sleep(Duration::from_millis(10)).await; // give it a bit of time to send the messages properly let mut desktop_files: Vec = get_desktop_files() .filter_map(|d| parse_desktop_file(d).ok()) .filter(|d| !d.no_display) .collect(); desktop_files.sort_by_key(|d| d.clone().name.unwrap_or_default()); let mut apps = Vec::new(); let mut radius = 1; while !desktop_files.is_empty() { let mut hex = HEX_CENTER.add(&HEX_DIRECTION_VECTORS[4].clone().scale(radius)); for i in 0..6 { if desktop_files.is_empty() { break; }; for _ in 0..radius { if desktop_files.is_empty() { break; }; apps.push( App::create_from_desktop_file( button.grabbable.content_parent(), hex.get_coords(), desktop_files.pop().unwrap(), &state, ) .unwrap(), ); hex = hex.neighbor(i); } } radius += 1; } AppHexGrid { movable_root, apps, button, state, } } } impl RootHandler for AppHexGrid { fn frame(&mut self, info: FrameInfo) { self.button.frame(&info); if self.button.button.pressed() { self.button .model .part("Hex") .unwrap() .set_material_parameter("color", MaterialParameter::Color(BTN_SELECTED_COLOR)) .unwrap(); self.state.unfurled = !self.state.unfurled; for app in &mut self.apps { app.apply_state(&self.state); } } else if self.button.button.released() { self.button .model .part("Hex") .unwrap() .set_material_parameter("color", MaterialParameter::Color(BTN_COLOR)) .unwrap(); } for app in &mut self.apps { app.frame(&info, &self.state); } } fn save_state(&mut self) -> Result { self.movable_root .set_relative_transform( self.button.grabbable.content_parent(), Transform::from_translation([0.0; 3]), ) .unwrap(); ClientState::new( Some(self.state.clone()), &self.movable_root, [( "content_parent".to_string(), self.button.grabbable.content_parent(), )] .into_iter() .collect(), ) } } struct CenterButton { button: Button, grabbable: Grabbable, model: Model, } impl CenterButton { fn new(client: &Arc, state: &ClientState) -> Result { // (APP_SIZE + PADDING) / 2.0, let button = Button::create( client.get_root(), Transform::identity(), [(APP_SIZE + PADDING) / 2.0; 2], ButtonSettings { visuals: None, ..Default::default() }, )?; let grabbable = Grabbable::create( client.get_root(), Transform::none(), &button.touch_plane().field(), GrabbableSettings { max_distance: 0.025, pointer_mode: PointerMode::Align, magnet: false, ..Default::default() }, )?; button .touch_plane() .root() .set_spatial_parent(grabbable.content_parent())?; let model = Model::create( grabbable.content_parent(), Transform::from_rotation_scale( Quat::from_rotation_x(PI / 2.0) * Quat::from_rotation_y(PI), [MODEL_SCALE; 3], ), &ResourceID::new_namespaced("protostar", "hexagon/hexagon"), )?; model .part("Hex")? .set_material_parameter("color", MaterialParameter::Color(BTN_COLOR))?; if let Some(content_parent) = state.spatial_anchors(client).get("content_parent") { grabbable .content_parent() .set_relative_transform(content_parent, Transform::identity())?; } Ok(CenterButton { button, grabbable, model, }) } fn frame(&mut self, info: &FrameInfo) { let _ = self.grabbable.update(&info); self.button.update(); } }