Compare commits

..

8 Commits

Author SHA1 Message Date
AnnoyingRains
3dc32ed3ae Merge branch 'StardustXR:dev' into dev 2025-08-31 02:18:41 +10:00
Schmarni
01ec90c699 fix(hexagon_launcher): use the proper resource init function
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-08-30 18:15:47 +02:00
Schmarni
e60463081e Revert "fix hexagon_launcher crashing (#8)"
This reverts commit 62b0fe8240.
This fix only stopped hexagon_launcher from exiting properly upon
receiving a fatal error
2025-08-30 18:09:23 +02:00
AnnoyingRains
cbd99c2cc7 fix: nix 2025-08-31 01:40:41 +10:00
Leah Anderson
62b0fe8240 fix hexagon_launcher crashing (#8) 2025-07-05 13:38:39 -07:00
Schmarni
f5008ff5b5 refactor: update everything to latest fusion and to rust edition 2024 (#7)
Signed-off-by: Schmarni <marnistromer@gmail.com>
2025-04-07 13:59:41 -07:00
Cyberneticmelon
e28a8e8e1e Updated documentation 2025-04-03 17:40:37 -04:00
Nova
c5c4840452 feat: update to latest fusion 2025-02-21 14:13:32 -08:00
17 changed files with 572 additions and 447 deletions

721
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
[workspace]
resolver = "2"
members = ["app_grid", "hexagon_launcher", "protostar", "single", "sirius"]
[workspace.dependencies]

View File

@@ -1,11 +1,24 @@
# protostar
A collection of application launchers for Stardust XR
> [!IMPORTANT]
> Requires the [Stardust XR Server](https://github.com/StardustXR/server) to be running. For launching 2D applications, [Flatland](https://github.com/StardustXR/flatland) also needs to be running.
Prototype application launcher for Stardust XR
If you installed the Stardust XR server via:
```note
sudo dnf group install stardust-xr
```
Or if you installed via the [installation script](https://github.com/cyberneticmelon/usefulscripts/blob/main/stardustxr_setup.sh), Protostar comes pre-installed
Protostar provides an easy to use crate to write applications launchers. See the [examples](examples) to learn more!
# How to Use
Protostar itself can be used to build various kinds of app launchers, but two are built in. Most likely the one you will want to use will be `hexagon_launcher`. After launching, in flastcreen mode drag applications out of the app launcher, hold down `Shift + ~`
![updated_drag](https://github.com/StardustXR/website/blob/main/static/img/updated_flat_drag.GIF)
**Quest 3 Hand tracking**:
Pinch to drag and drop, grasp with full hand for grabbing, point and click with pointer finger to click or pinch from a distance
# TODO:
![hand_pinching](https://github.com/StardustXR/website/blob/main/static/img/hand_pinching.GIF)
1. Implement our own (more reliable) way to get icons
2. Untangle the code (eg: make it so that the user can decide sizes, models, etc... )
3. Implement [configuration files](https://docs.rs/confy/latest/confy/) in the examples
## Manual Installation
Clone the repository and after the server is running:
```sh
cargo run
```

View File

@@ -1,7 +1,7 @@
[package]
name = "app_grid"
version = "0.1.0"
edition = "2021"
edition = "2024"
[dependencies]
tokio = { version = "1.32.0", features = ["rt", "tokio-macros", "sync"] }

View File

@@ -13,11 +13,11 @@ use stardust_xr_fusion::{
YAlign,
},
fields::{Field, Shape},
node::NodeType,
root::{ClientState, FrameInfo, RootAspect, RootHandler},
root::{ClientState, FrameInfo, RootAspect},
spatial::{Spatial, SpatialAspect, SpatialRefAspect, Transform},
ClientHandle,
};
use stardust_xr_molecules::{Grabbable, GrabbableSettings};
use stardust_xr_molecules::{FrameSensitive, Grabbable, GrabbableSettings, UIElement};
use std::f32::consts::PI;
const APP_LIMIT: usize = 300;
@@ -32,16 +32,34 @@ async fn main() -> Result<()> {
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.pretty()
.init();
let (client, event_loop) = Client::connect_with_async_loop().await?;
let owned_client = Client::connect().await?;
let client = owned_client.handle();
let async_loop = owned_client.async_event_loop();
client
.set_base_prefixes(&[directory_relative_path!("../res")])
.get_root()
.set_base_prefixes(&[directory_relative_path!("../res").to_string()])
.unwrap();
let _root = client.get_root().alias().wrap(AppGrid::new(&client))?;
let mut grid = AppGrid::new(&client);
let mut owned_client = async_loop.stop().await.unwrap();
let event_loop = owned_client.sync_event_loop(|handle, _| {
let Some(event) = handle.get_root().recv_root_event() else {
return;
};
match event {
stardust_xr_fusion::root::RootEvent::Ping { response } => response.send(Ok(())),
stardust_xr_fusion::root::RootEvent::Frame { info } => {
grid.frame(info);
}
stardust_xr_fusion::root::RootEvent::SaveState { response } => {
response.send(grid.save_state());
}
}
});
tokio::select! {
_ = tokio::signal::ctrl_c() => (),
e = event_loop => e??,
e = event_loop => e?,
};
Ok(())
}
@@ -51,7 +69,7 @@ struct AppGrid {
//style: TextStyle,
}
impl AppGrid {
fn new(client: &Client) -> Self {
fn new(client: &ClientHandle) -> Self {
let apps = get_desktop_files()
.filter_map(|d| parse_desktop_file(d).ok())
.filter(|d| !d.no_display)
@@ -74,7 +92,7 @@ impl AppGrid {
AppGrid { apps }
}
}
impl RootHandler for AppGrid {
impl AppGrid {
fn frame(&mut self, info: FrameInfo) {
for app in &mut self.apps {
app.frame(&info);
@@ -198,7 +216,10 @@ impl App {
// }
fn frame(&mut self, info: &FrameInfo) {
let _ = self.grabbable.update(info);
if !self.grabbable.handle_events() {
return;
}
self.grabbable.frame(info);
if self.grabbable.grab_action().actor_stopped() {
self.grabbable.cancel_angular_velocity();
@@ -210,8 +231,8 @@ impl App {
// }
let application = self.application.clone();
let space = self.content_parent().alias();
let root = self.root.alias();
let space = self.content_parent().clone();
let root = self.root.clone();
tokio::task::spawn(async move {
let Ok(transform) = space.get_transform(&root).await else {

17
flake.lock generated
View File

@@ -1,17 +1,12 @@
{
"nodes": {
"crane": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1712681629,
"narHash": "sha256-bMDXn4AkTXLCpoZbII6pDGoSeSe9gI87jxPsHRXgu/E=",
"lastModified": 1755993354,
"narHash": "sha256-FCRRAzSaL/+umLIm3RU3O/+fJ2ssaPHseI2SSFL8yZU=",
"owner": "ipetkov",
"repo": "crane",
"rev": "220387ac8e99cbee0ca4c95b621c4bc782b6a235",
"rev": "25bd41b24426c7734278c2ff02e53258851db914",
"type": "github"
},
"original": {
@@ -22,11 +17,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1712791164,
"narHash": "sha256-3sbWO1mbpWsLepZGbWaMovSO7ndZeFqDSdX0hZ9nVyw=",
"lastModified": 1756386758,
"narHash": "sha256-1wxxznpW2CKvI9VdniaUnTT2Os6rdRJcRUf65ZK9OtE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1042fd8b148a9105f3c0aca3a6177fd1d9360ba5",
"rev": "dfb2f12e899db4876308eba6d93455ab7da304cd",
"type": "github"
},
"original": {

View File

@@ -2,7 +2,6 @@
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
crane = {
inputs.nixpkgs.follows = "nixpkgs";
url = "github:ipetkov/crane";
};
};
@@ -13,9 +12,9 @@
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
in {
packages = forAllSystems (system: let pkgs = nixpkgsFor.${system}; in {
default = crane.lib.${system}.buildPackage {
pname = "protostar";
packages = forAllSystems (system: let pkgs = nixpkgsFor.${system}; craneLib = crane.mkLib pkgs; in {
default = craneLib.buildPackage {
pname = "hexagon-launcher";
version = "0.1.0";
src = ./.;

View File

@@ -1,7 +1,7 @@
[package]
name = "hexagon_launcher"
version = "0.1.0"
edition = "2021"
edition = "2024"
[dependencies]
protostar = { path = "../protostar" }

View File

@@ -15,7 +15,7 @@ use stardust_xr_fusion::{
root::FrameInfo,
spatial::{Spatial, SpatialAspect, SpatialRefAspect, Transform},
};
use stardust_xr_molecules::{Grabbable, GrabbableSettings};
use stardust_xr_molecules::{FrameSensitive as _, Grabbable, GrabbableSettings, UIElement};
use std::f32::consts::PI;
use tween::{QuartInOut, Tweener};
@@ -160,7 +160,7 @@ impl App {
}
Ok(App {
parent: parent.alias(),
parent: parent.clone(),
position,
grabbable,
_field: field,
@@ -190,7 +190,9 @@ impl App {
}
pub fn frame(&mut self, info: &FrameInfo, state: &State) {
let _ = self.grabbable.update(info);
if self.grabbable.handle_events() {
self.grabbable.frame(info);
}
if let Some(grabbable_move) = &mut self.grabbable_move {
if !grabbable_move.is_finished() {
@@ -265,8 +267,8 @@ impl App {
self.grabbable_shrink = Some(Tweener::quart_in_out(APP_SIZE * 0.5, 0.0001, 0.25));
let application = self.application.clone();
let space = self.content_parent().alias();
let parent = self.parent.alias();
let space = self.content_parent().clone();
let parent = self.parent.clone();
//TODO: split the executable string for the args
tokio::task::spawn(async move {

View File

@@ -5,23 +5,24 @@ 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 protostar::xdg::{DesktopFile, get_desktop_files, parse_desktop_file};
use serde::{Deserialize, Serialize};
use stardust_xr_fusion::{
ClientHandle,
client::Client,
core::values::{
color::{color_space::LinearRgb, rgba_linear, Rgba},
ResourceID,
color::{Rgba, color_space::LinearRgb, rgba_linear},
},
drawable::{MaterialParameter, Model, ModelPartAspect},
node::{NodeError, NodeType},
root::{ClientState, FrameInfo, RootAspect, RootHandler},
node::NodeError,
project_local_resources,
root::{ClientState, FrameInfo, RootAspect, RootEvent},
spatial::{Spatial, SpatialAspect, Transform},
};
use stardust_xr_molecules::{
FrameSensitive, Grabbable, GrabbableSettings, PointerMode, UIElement,
button::{Button, ButtonSettings},
Grabbable, GrabbableSettings, PointerMode,
};
use std::{f32::consts::PI, sync::Arc, time::Duration};
@@ -41,19 +42,30 @@ async fn main() -> Result<()> {
.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)?;
let owned_client = Client::connect().await?;
owned_client.setup_resources(&[&project_local_resources!("../res")])?;
let client = owned_client.handle();
let async_loop = owned_client.async_event_loop();
let mut grid = AppHexGrid::new(&client).await;
let mut owned_client = async_loop.stop().await.unwrap();
let event_loop = owned_client.sync_event_loop(|handle, _| {
let Some(event) = handle.get_root().recv_root_event() else {
return;
};
match event {
RootEvent::Ping { response } => response.send(Ok(())),
RootEvent::Frame { info } => {
grid.frame(info);
}
RootEvent::SaveState { response } => {
response.send(grid.save_state());
}
}
});
tokio::select! {
_ = tokio::signal::ctrl_c() => (),
e = event_loop => e??,
e = event_loop => e?,
}
Ok(())
}
@@ -70,13 +82,14 @@ struct AppHexGrid {
state: State,
}
impl AppHexGrid {
async fn new(client: &Arc<Client>) -> Self {
let state = client.get_state().data().unwrap_or_default();
async fn new(client: &Arc<ClientHandle>) -> Self {
let client_state = client.get_root().get_state().await.unwrap();
let state = client_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();
let button = CenterButton::new(client, &client_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<DesktopFile> = get_desktop_files()
@@ -120,7 +133,7 @@ impl AppHexGrid {
}
}
}
impl RootHandler for AppHexGrid {
impl AppHexGrid {
fn frame(&mut self, info: FrameInfo) {
self.button.frame(&info);
if self.button.button.pressed() {
@@ -173,7 +186,7 @@ struct CenterButton {
model: Model,
}
impl CenterButton {
fn new(client: &Arc<Client>, state: &ClientState) -> Result<Self, NodeError> {
fn new(client: &Arc<ClientHandle>, state: &ClientState) -> Result<Self, NodeError> {
// (APP_SIZE + PADDING) / 2.0,
let button = Button::create(
client.get_root(),
@@ -224,7 +237,9 @@ impl CenterButton {
}
fn frame(&mut self, info: &FrameInfo) {
let _ = self.grabbable.update(info);
self.button.update();
if self.grabbable.handle_events() {
self.grabbable.frame(info);
}
self.button.handle_events();
}
}

View File

@@ -1,7 +1,7 @@
[package]
name = "protostar"
version = "0.4.0"
edition = "2021"
edition = "2024"
[dependencies]
clap = { version = "4.1.3", features = ["derive"] }

View File

@@ -44,9 +44,9 @@ impl Application {
icon.and_then(|i| i.cached_process(preferred_px_size).ok())
}
pub fn launch(&self, launch_space: &impl SpatialRefAspect) -> NodeResult<()> {
pub fn launch<T: SpatialRefAspect + Clone>(&self, launch_space: &T) -> NodeResult<()> {
let client = launch_space.node().client()?;
let launch_space = launch_space.alias();
let launch_space = launch_space.clone();
let executable = self
.desktop_file
@@ -66,10 +66,16 @@ impl Application {
return;
};
for (k, v) in connection_env.into_iter() {
std::env::set_var(k, v);
// this should be fine, probably?
unsafe {
std::env::set_var(k, v);
}
}
std::env::set_var("STARDUST_STARTUP_TOKEN", startup_token);
// this should be fine, probably?
unsafe {
std::env::set_var("STARDUST_STARTUP_TOKEN", startup_token);
}
// Strip/ignore field codes https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s07.html
let re = Regex::new(r"%[fFuUdDnNickvm]").unwrap();

View File

@@ -1,7 +1,7 @@
[package]
name = "single"
version = "0.1.0"
edition = "2021"
edition = "2024"
[dependencies]
protostar = { path = "../protostar" }

View File

@@ -4,7 +4,7 @@ use clap::Parser;
use color_eyre::{eyre::Result, Report};
use manifest_dir_macros::directory_relative_path;
use protostar::xdg::parse_desktop_file;
use stardust_xr_fusion::{client::Client, node::NodeType, root::RootAspect};
use stardust_xr_fusion::{client::Client, root::RootAspect};
use std::path::PathBuf;
use tracing_subscriber::EnvFilter;
@@ -25,20 +25,38 @@ async fn main() -> Result<()> {
.init();
color_eyre::install()?;
let args = Args::parse();
let (client, event_loop) = Client::connect_with_async_loop().await?;
client.set_base_prefixes(&[directory_relative_path!("../res")])?;
let owned_client = Client::connect().await?;
let client = owned_client.handle();
let async_loop = owned_client.async_event_loop();
client
.get_root()
.set_base_prefixes(&[directory_relative_path!("../res").to_string()])?;
let protostar = Single::create_from_desktop_file(
let mut protostar = Single::create_from_desktop_file(
client.get_root(),
[0.0, 0.0, 0.0],
parse_desktop_file(args.desktop_file).map_err(Report::msg)?,
)?;
let _root = client.get_root().alias().wrap(protostar)?;
let mut owned_client = async_loop.stop().await.unwrap();
let event_loop = owned_client.sync_event_loop(|handle, _| {
let Some(event) = handle.get_root().recv_root_event() else {
return;
};
match event {
stardust_xr_fusion::root::RootEvent::Ping { response } => response.send(Ok(())),
stardust_xr_fusion::root::RootEvent::Frame { info } => {
protostar.frame(info);
}
stardust_xr_fusion::root::RootEvent::SaveState { response } => {
response.send(protostar.save_state());
}
}
});
tokio::select! {
_ = tokio::signal::ctrl_c() => (),
e = event_loop => e??,
e = event_loop => e?,
};
Ok(())
}

View File

@@ -12,10 +12,10 @@ use stardust_xr_fusion::{
},
fields::{Field, Shape},
node::NodeType,
root::{ClientState, FrameInfo, RootHandler},
root::{ClientState, FrameInfo},
spatial::{Spatial, SpatialAspect, SpatialRefAspect, Transform},
};
use stardust_xr_molecules::{Grabbable, GrabbableSettings};
use stardust_xr_molecules::{FrameSensitive, Grabbable, GrabbableSettings, UIElement};
use std::f32::consts::PI;
use tween::{QuartInOut, Tweener};
@@ -146,9 +146,11 @@ impl Single {
self.grabbable.content_parent()
}
}
impl RootHandler for Single {
fn frame(&mut self, info: FrameInfo) {
let _ = self.grabbable.update(&info);
impl Single {
pub fn frame(&mut self, info: FrameInfo) {
if self.grabbable.handle_events() {
self.grabbable.frame(&info);
}
if let Some(grabbable_move) = &mut self.grabbable_move {
if !grabbable_move.is_finished() {
@@ -224,8 +226,8 @@ impl RootHandler for Single {
self.grabbable_shrink = Some(Tweener::quart_in_out(MODEL_SCALE, 0.0001, 0.25));
let application = self.application.clone();
let space = self.content_parent().alias();
let root = self.root.alias();
let space = self.content_parent().clone();
let root = self.root.clone();
//TODO: split the executable string for the args
tokio::task::spawn(async move {
@@ -244,7 +246,7 @@ impl RootHandler for Single {
}
}
fn save_state(&mut self) -> color_eyre::eyre::Result<ClientState> {
pub fn save_state(&mut self) -> color_eyre::eyre::Result<ClientState> {
ClientState::from_root(self.content_parent())
}
}

View File

@@ -1,7 +1,7 @@
[package]
name = "sirius"
version = "0.1.0"
edition = "2021"
edition = "2024"
[dependencies]
protostar = { path = "../protostar" }

View File

@@ -16,12 +16,12 @@ use stardust_xr_fusion::{
},
fields::{Field, Shape},
node::{NodeError, NodeType},
root::{ClientState, FrameInfo, RootAspect, RootHandler},
spatial::{Spatial, SpatialAspect, SpatialRefAspect, Transform},
root::{ClientState, FrameInfo, RootAspect},
spatial::{Spatial, SpatialAspect, SpatialRefAspect, Transform}, ClientHandle,
};
use stardust_xr_molecules::{
button::{Button, ButtonSettings},
Grabbable, GrabbableSettings,
FrameSensitive, Grabbable, GrabbableSettings, UIElement,
};
use std::{f32::consts::PI, path::PathBuf};
@@ -50,17 +50,33 @@ async fn main() -> Result<()> {
)
}
let (client, event_loop) = Client::connect_with_async_loop().await?;
client.set_base_prefixes(&[directory_relative_path!("../res")])?;
let _wrapped_root = client
let owned_client = Client::connect().await?;
let client = owned_client.handle();
let async_loop = owned_client.async_event_loop();
client
.get_root()
.alias()
.wrap(Sirius::new(&client, args)?)?;
.set_base_prefixes(&[directory_relative_path!("../res").to_string()])?;
let mut sirius = Sirius::new(&client, args)?;
let mut owned_client = async_loop.stop().await.unwrap();
let event_loop = owned_client.sync_event_loop(|handle, _| {
let Some(event) = handle.get_root().recv_root_event() else {
return;
};
match event {
stardust_xr_fusion::root::RootEvent::Ping { response } => response.send(Ok(())),
stardust_xr_fusion::root::RootEvent::Frame { info } => {
sirius.frame(info);
}
stardust_xr_fusion::root::RootEvent::SaveState { response } => {
response.send(sirius.save_state());
}
}
});
tokio::select! {
_ = tokio::signal::ctrl_c() => (),
e = event_loop => e??,
e = event_loop => e?,
}
Ok(())
}
@@ -78,7 +94,7 @@ struct Sirius {
grabbable: Grabbable,
}
impl Sirius {
fn new(client: &Client, args: Args) -> Result<Self, NodeError> {
fn new(client: &ClientHandle, args: Args) -> Result<Self, NodeError> {
let root = Spatial::create(client.get_root(), Transform::identity(), false).unwrap();
let field =
@@ -141,15 +157,16 @@ impl Sirius {
// }
// }
}
impl RootHandler for Sirius {
impl Sirius {
fn frame(&mut self, info: FrameInfo) {
for app in &mut self.clients {
app.frame(&info);
}
self.grabbable.update(&info).unwrap();
self.button.update();
if self.button.pressed() {
if self.grabbable.handle_events() {
self.grabbable.frame(&info);
};
if self.button.handle_events() && self.button.pressed() {
println!("Touch started");
self.state.visible = !self.state.visible;
match self.state.visible {
@@ -343,7 +360,7 @@ impl App {
.ok()
});
Ok(App {
parent: parent.alias(),
parent: parent.clone(),
position,
grabbable,
_field: field,
@@ -374,7 +391,10 @@ impl App {
}
fn frame(&mut self, info: &FrameInfo) {
let _ = self.grabbable.update(info);
if !self.grabbable.handle_events() {
return;
}
self.grabbable.frame(info);
if let Some(grabbable_move) = &mut self.grabbable_move {
if !grabbable_move.is_finished() {
@@ -449,8 +469,8 @@ impl App {
self.grabbable_shrink = Some(Tweener::quart_in_out(APP_SIZE * 0.5, 0.0001, 0.25));
let application = self.application.clone();
let space = self.content_parent().alias();
let parent = self.parent.alias();
let space = self.content_parent().clone();
let parent = self.parent.clone();
//TODO: split the executable string for the args
tokio::task::spawn(async move {