use crate::{ core::{client::Client, destroy_queue, registry::Registry, resource::ResourceID}, nodes::{ spatial::{find_spatial_parent, parse_transform, Spatial}, Node, }, }; use anyhow::{anyhow, ensure, Result}; use glam::{vec3, Mat4, Vec2}; use mint::Vector2; use once_cell::sync::OnceCell; use parking_lot::Mutex; use prisma::{Flatten, Rgb, Rgba}; use send_wrapper::SendWrapper; use serde::Deserialize; use stardust_xr::{schemas::flex::deserialize, values::Transform}; use std::{path::PathBuf, sync::Arc}; use stereokit::{ font::Font, lifecycle::DrawContext, text::{self, TextAlign, TextFit, TextStyle}, StereoKit, }; static TEXT_REGISTRY: Registry = Registry::new(); struct TextData { text: String, character_height: f32, text_align: TextAlign, bounds: Option, fit: TextFit, bounds_align: TextAlign, color: Rgba, } pub struct Text { space: Arc, font_path: Option, style: OnceCell>, data: Mutex, } impl Text { #[allow(clippy::too_many_arguments)] pub fn add_to( node: &Arc, font_resource_id: Option, text: String, character_height: f32, text_align: TextAlign, bounds: Option>, fit: TextFit, bounds_align: TextAlign, color: Rgba, ) -> Result> { ensure!( node.spatial.get().is_some(), "Internal: Node does not have a spatial attached!" ); ensure!( node.model.get().is_none(), "Internal: Node already has text attached!" ); let client = node .get_client() .ok_or_else(|| anyhow!("Client not found"))?; let text = TEXT_REGISTRY.add(Text { space: node.spatial.get().unwrap().clone(), font_path: font_resource_id .and_then(|res| res.get_file(&client.base_resource_prefixes.lock().clone())), style: OnceCell::new(), data: Mutex::new(TextData { text, character_height, text_align, bounds: bounds.map(|b| b.into()), fit, bounds_align, color, }), }); node.add_local_signal("set_character_height", Text::set_character_height_flex); node.add_local_signal("set_text", Text::set_text_flex); let _ = node.text.set(text.clone()); Ok(text) } fn draw(&self, sk: &StereoKit, draw_ctx: &DrawContext) { let style = self.style .get_or_try_init(|| -> Result, anyhow::Error> { let font = if let Some(path) = self.font_path.as_deref() { Font::from_file(sk, path) } else { Some(Font::default(sk)) }; Ok(SendWrapper::new(TextStyle::new( sk, font.ok_or(std::fmt::Error)?, 1.0, Rgba::new(Rgb::new(1.0, 1.0, 1.0), 1.0), ))) }); if let Ok(style) = style { let data = self.data.lock(); let transform = self.space.global_transform() * Mat4::from_scale(vec3( data.character_height, data.character_height, data.character_height, )); if let Some(bounds) = data.bounds { text::draw_in( draw_ctx, &data.text, transform, bounds / data.character_height, data.fit, style, data.bounds_align, data.text_align, vec3(0.0, 0.0, 0.0), data.color, ); } else { text::draw_at( draw_ctx, &data.text, transform, style, data.bounds_align, data.text_align, vec3(0.0, 0.0, 0.0), data.color, ); } } } pub fn set_character_height_flex( node: &Node, _calling_client: Arc, data: &[u8], ) -> Result<()> { let height = flexbuffers::Reader::get_root(data)?.get_f64()? as f32; node.text.get().unwrap().data.lock().character_height = height; Ok(()) } pub fn set_text_flex(node: &Node, _calling_client: Arc, data: &[u8]) -> Result<()> { let text = flexbuffers::Reader::get_root(data)?.get_str()?.to_string(); node.text.get().unwrap().data.lock().text = text; Ok(()) } } impl Drop for Text { fn drop(&mut self) { if let Some(style) = self.style.take() { destroy_queue::add(style); } TEXT_REGISTRY.remove(self); } } pub fn draw_all(sk: &StereoKit, draw_ctx: &DrawContext) { for text in TEXT_REGISTRY.get_valid_contents() { text.draw(sk, draw_ctx); } } pub fn create_flex(_node: &Node, calling_client: Arc, data: &[u8]) -> Result<()> { #[derive(Deserialize)] struct CreateTextInfo<'a> { name: &'a str, parent_path: &'a str, transform: Transform, text: String, font_resource: Option, character_height: f32, text_align: TextAlign, bounds: Option>, fit: TextFit, bounds_align: TextAlign, color: [f32; 4], } let info: CreateTextInfo = deserialize(data)?; let node = Node::create(&calling_client, "/drawable/text", info.name, true); let parent = find_spatial_parent(&calling_client, info.parent_path)?; let transform = parse_transform(info.transform, true, true, true)?; let color = Rgba::from_slice(&info.color); let node = node.add_to_scenegraph(); Spatial::add_to(&node, Some(parent), transform, false)?; Text::add_to( &node, info.font_resource, info.text, info.character_height, info.text_align, info.bounds, info.fit, info.bounds_align, color, )?; Ok(()) }