stereokit_rust/
sprite.rs

1use crate::{
2    StereoKitError,
3    maths::{Matrix, Vec2},
4    sk::MainThreadToken,
5    system::{IAsset, Pivot},
6    tex::{Tex, TexT},
7    util::Color32,
8};
9use std::{
10    ffi::{CStr, CString, c_char},
11    path::Path,
12    ptr::NonNull,
13};
14
15/// The way the Sprite is stored on the backend! Does it get batched and atlased for draw efficiency, or is it a single image?
16#[derive(Debug, Copy, Clone, PartialEq, Eq)]
17#[repr(u32)]
18pub enum SpriteType {
19    /// The sprite will be batched onto an atlas texture so all sprites can be drawn in a single pass. This is excellent for performance! The only thing to watch out for here, adding a sprite to an atlas will rebuild the atlas texture! This can be a bit expensive, so it’s recommended to add all sprites to an atlas at start, rather than during runtime. Also, if an image is too large, it may take up too much space on the atlas, and may be better as a Single sprite type.
20    Atlased = 0,
21    /// This sprite is on its own texture. This is best for large images, items that get loaded and unloaded during runtime, or for sprites that may have edge artifacts or severe ‘bleed’ from adjacent atlased images.
22    Single = 1,
23}
24
25/// A Sprite is an image that’s set up for direct 2D rendering, without using a mesh or model! This is technically a
26/// wrapper over a texture, but it also includes atlasing functionality, which can be pretty important to performance!
27/// This is used a lot in UI, for image rendering.
28///
29/// Atlasing is not currently implemented, it’ll swap to Single for now. But here’s how it works!
30/// StereoKit will batch your sprites into an atlas if you ask it to! This puts all the images on a single texture to
31/// significantly reduce draw calls when many images are present. Any time you add a sprite to an atlas, it’ll be marked
32/// as dirty and rebuilt at the end of the frame. So it can be a good idea to add all your images to the atlas on
33/// initialize rather than during execution!
34///
35/// Since rendering is atlas based, you also have only one material per atlas. So this is why you might wish to put a
36/// sprite in one atlas or another, so you can apply different
37/// <https://stereokit.net/Pages/StereoKit/Sprite.html>
38///
39/// ### Examples
40/// ```
41/// # stereokit_rust::test_init_sk!(); // !!!! Get a proper way to initialize sk !!!!
42/// use stereokit_rust::{maths::{Vec3, Matrix}, sprite::{Sprite, SpriteType},
43///                      tex::Tex, util::{Gradient, Color128, named_colors}, system::Pivot};
44/// let mut gradient = Gradient::new(None);
45/// gradient
46///     .add(Color128::BLACK_TRANSPARENT, 0.0)
47///     .add(named_colors::YELLOW, 0.1)
48///     .add(named_colors::LIGHT_BLUE, 0.4)
49///     .add(named_colors::BLUE, 0.5)
50///     .add(Color128::BLACK, 0.7);
51/// let tex_particule1 = Tex::gen_particle(128, 128, 0.2, Some(gradient));
52/// let tex_particule2 = Tex::gen_particle(128, 128, 0.2, None);
53///
54/// let sprite1 = Sprite::from_tex(&tex_particule1, None, None)
55///                   .expect("Should be able to create sprite");
56/// let sprite2 = Sprite::from_file("icons/fly_over.png", Some(SpriteType::Single), Some("MY_ID"))
57///                   .expect("open_gltf.jpeg should be able to create sprite");
58/// let sprite3 = sprite1.clone_ref();
59/// let sprite4 = Sprite::from_tex(&tex_particule2, None, None)
60///                   .expect("Should be able to create sprite");
61///
62/// let transform1 = Matrix::t([-0.7, 0.4, 0.0]) * Matrix::Y_180;
63/// let transform2 = Matrix::t([ 0.7, 0.4, 0.0]) * Matrix::Y_180;
64/// let transform3 = Matrix::t([-0.7,-0.4, 0.0]) * Matrix::Y_180;
65/// let transform4 = Matrix::t([ 0.7,-0.4, 0.0]) * Matrix::Y_180;
66///
67/// filename_scr = "screenshots/sprite.jpeg";
68/// test_screenshot!( // !!!! Get a proper main loop !!!!
69///     sprite1.draw(token, transform1, Pivot::Center, None);
70///     sprite2.draw(token, transform2, Pivot::XLeft, Some(named_colors::AZURE.into()));
71///     sprite3.draw(token, transform3, Pivot::TopRight, Some(named_colors::LIME.into()));
72///     sprite4.draw(token, transform4, Pivot::YCenter, None);
73/// );
74/// ```
75/// <img src="https://raw.githubusercontent.com/mvvvv/StereoKit-rust/refs/heads/master/screenshots/sprite.jpeg" alt="screenshot" width="200">
76#[repr(C)]
77#[derive(Debug, PartialEq)]
78pub struct Sprite(pub NonNull<_SpriteT>);
79
80impl Drop for Sprite {
81    fn drop(&mut self) {
82        unsafe { sprite_release(self.0.as_ptr()) };
83    }
84}
85
86impl AsRef<Sprite> for Sprite {
87    fn as_ref(&self) -> &Sprite {
88        self
89    }
90}
91
92/// StereoKit internal type.
93#[repr(C)]
94#[derive(Debug)]
95pub struct _SpriteT {
96    _unused: [u8; 0],
97}
98
99/// StereoKit ffi type.
100pub type SpriteT = *mut _SpriteT;
101
102unsafe extern "C" {
103    pub fn sprite_find(id: *const c_char) -> SpriteT;
104    pub fn sprite_create(sprite: TexT, type_: SpriteType, atlas_id: *const c_char) -> SpriteT;
105    pub fn sprite_create_file(filename_utf8: *const c_char, type_: SpriteType, atlas_id: *const c_char) -> SpriteT;
106
107    pub fn sprite_set_id(sprite: SpriteT, id: *const c_char);
108
109    pub fn sprite_get_id(sprite: SpriteT) -> *const c_char;
110    pub fn sprite_addref(sprite: SpriteT);
111    pub fn sprite_release(sprite: SpriteT);
112    pub fn sprite_get_aspect(sprite: SpriteT) -> f32;
113    pub fn sprite_get_width(sprite: SpriteT) -> i32;
114    pub fn sprite_get_height(sprite: SpriteT) -> i32;
115    pub fn sprite_get_dimensions_normalized(sprite: SpriteT) -> Vec2;
116    pub fn sprite_draw(sprite: SpriteT, transform: Matrix, pivot_position: Pivot, color: Color32);
117}
118
119impl IAsset for Sprite {
120    // fn id(&mut self, id: impl AsRef<str>) {
121    //     self.id(id);
122    // }
123
124    fn get_id(&self) -> &str {
125        self.get_id()
126    }
127}
128
129/// A Default sprite is asked when a Sprite creation or find returned an error. (close is the default sprite)
130impl Default for Sprite {
131    fn default() -> Self {
132        Self::close()
133    }
134}
135
136impl Sprite {
137    /// Create a sprite from a texture.
138    /// <https://stereokit.net/Pages/StereoKit/Sprite/FromTex.html>
139    /// * `tex` - The texture to build a sprite from. Must be a valid, 2D image!
140    /// * `sprite_type` - Should this sprite be atlased, or an individual image? Adding this as an atlased image is better for
141    ///   performance, but will cause the atlas to be rebuilt! Images that take up too much space on the atlas, or might
142    ///   be loaded or unloaded during runtime may be better as Single rather than Atlased!
143    ///   If None has default of Atlased.
144    /// * `atlas_id` - The name of which atlas the sprite should belong to, this is only relevant if the SpriteType is
145    ///   Atlased. If None has default of "default".
146    ///
147    /// Returns a  Sprite asset, or null if the image failed when adding to the sprite system!
148    /// see also [`sprite_create`]
149    /// ### Examples
150    /// ```
151    /// # stereokit_rust::test_init_sk!();
152    /// use stereokit_rust::{ maths::Matrix, sprite::{Sprite, SpriteType}, tex::Tex, system::Pivot,
153    ///                      util::{Gradient, Color128, named_colors}};
154    ///
155    /// let mut gradient = Gradient::new(None);
156    /// gradient
157    ///     .add(Color128::BLACK_TRANSPARENT, 0.0)
158    ///     .add(named_colors::YELLOW, 0.1)
159    ///     .add(named_colors::LIGHT_BLUE, 0.4)
160    ///     .add(named_colors::BLUE, 0.5)
161    ///     .add(Color128::BLACK, 0.7);
162    /// let tex_particule1 = Tex::gen_particle(128, 128, 0.2, Some(gradient));
163    /// let sprite = Sprite::from_tex(&tex_particule1, Some(SpriteType::Atlased), Some("my_sprite".to_string()))
164    ///                   .expect("Should be able to create sprite");
165    ///
166    /// assert!(sprite.get_id().starts_with("auto/tex_"));
167    /// assert_eq!(sprite.get_height(), 128);
168    /// assert_eq!(sprite.get_width(), 128);
169    /// assert_eq!(sprite.get_normalized_dimensions(), [1.0, 1.0].into());
170    /// assert_eq!(sprite.get_aspect(), 1.0);
171    ///
172    /// filename_scr = "screenshots/sprite_from_tex.jpeg";
173    /// test_screenshot!( // !!!! Get a proper main loop !!!!
174    ///     sprite.draw(token, Matrix::Y_180, Pivot::XRight,  None);
175    ///     sprite.draw(token, Matrix::Y_180, Pivot::XLeft,   Some(named_colors::BLUE.into()));
176    ///     sprite.draw(token, Matrix::Y_180, Pivot::YTop,    Some(named_colors::RED.into()));
177    ///     sprite.draw(token, Matrix::Y_180, Pivot::YBottom, Some(named_colors::GREEN.into()));
178    /// );
179    /// ```
180    /// <img src="https://raw.githubusercontent.com/mvvvv/StereoKit-rust/refs/heads/master/screenshots/sprite_from_tex.jpeg" alt="screenshot" width="200">
181    pub fn from_tex(
182        sprite_tex: impl AsRef<Tex>,
183        sprite_type: Option<SpriteType>,
184        atlas_id: Option<String>,
185    ) -> Result<Sprite, StereoKitError> {
186        let sprite_type = sprite_type.unwrap_or(SpriteType::Atlased);
187        let atlas_id = match atlas_id {
188            Some(s) => s,
189            None => "default".to_owned(),
190        };
191        let c_atlas_id = CString::new(atlas_id)?;
192        Ok(Sprite(
193            NonNull::new(unsafe { sprite_create(sprite_tex.as_ref().0.as_ptr(), sprite_type, c_atlas_id.as_ptr()) })
194                .ok_or(StereoKitError::SpriteCreate)?,
195        ))
196    }
197
198    /// Create a sprite from an image file! This loads a Texture from file, and then uses that Texture as the source for the Sprite.
199    /// <https://stereokit.net/Pages/StereoKit/Sprite/FromFile.html>
200    /// * `file_utf8` - The filename of the image, an absolute filename, or a filename relative to the assets folder.
201    ///   Supports jpg, png, tga, bmp, psd, gif, hdr, pic.
202    /// * `sprite_type` - Should this sprite be atlased, or an individual image? Adding this as an atlased image is
203    ///   better for performance, but will cause the atlas to be rebuilt! Images that take up too much space on the
204    ///   atlas, or might be loaded or unloaded during runtime may be better as Single rather than Atlased!
205    ///   If None has default of Atlased
206    /// * `atlas_id` - The name of which atlas the sprite should belong to, this is only relevant if the SpriteType is
207    ///   Atlased. If None has default of "default".
208    ///
209    /// see also [`sprite_create_file`]
210    /// ### Examples
211    /// ```
212    /// # stereokit_rust::test_init_sk!();
213    /// use stereokit_rust::{ maths::Matrix, sprite::{Sprite, SpriteType}, system::Pivot,
214    ///                      util::named_colors};
215    ///
216    /// let sprite = Sprite::from_file("icons/log_viewer.png", Some(SpriteType::Single), Some("MY_ID"))
217    ///                   .expect("open_gltf.jpeg should be able to create sprite");
218    ///
219    /// assert_eq!(sprite.get_id(), "icons/log_viewer.png/sprite");
220    /// assert_eq!(sprite.get_height(), 128);
221    /// assert_eq!(sprite.get_width(), 128);
222    /// assert_eq!(sprite.get_normalized_dimensions().x, 1.0);
223    /// assert_eq!(sprite.get_normalized_dimensions().y, 1.0);
224    ///
225    /// filename_scr = "screenshots/sprite_from_file.jpeg";
226    /// test_screenshot!( // !!!! Get a proper main loop !!!!
227    ///     sprite.draw(token, Matrix::Y_180, Pivot::XRight,  None);
228    ///     sprite.draw(token, Matrix::Y_180, Pivot::XLeft,   Some(named_colors::BLUE.into()));
229    ///     sprite.draw(token, Matrix::Y_180, Pivot::YTop,    Some(named_colors::RED.into()));
230    ///     sprite.draw(token, Matrix::Y_180, Pivot::YBottom, Some(named_colors::GREEN.into()));
231    /// );
232    /// ```
233    /// <img src="https://raw.githubusercontent.com/mvvvv/StereoKit-rust/refs/heads/master/screenshots/sprite_from_file.jpeg" alt="screenshot" width="200">
234    pub fn from_file(
235        file_utf8: impl AsRef<Path>,
236        sprite_type: Option<SpriteType>,
237        atlas_id: Option<&str>,
238    ) -> Result<Sprite, StereoKitError> {
239        let sprite_type = sprite_type.unwrap_or(SpriteType::Atlased);
240        let atlas_id = match atlas_id {
241            Some(s) => s.to_owned(),
242            None => "default".to_owned(),
243        };
244        let c_atlas_id = CString::new(atlas_id)?;
245
246        let path_buf = file_utf8.as_ref().to_path_buf();
247        let c_str = CString::new(path_buf.clone().to_str().ok_or(StereoKitError::SpriteFile(path_buf.clone()))?)?;
248
249        Ok(Sprite(
250            NonNull::new(unsafe { sprite_create_file(c_str.as_ptr(), sprite_type, c_atlas_id.as_ptr()) })
251                .ok_or(StereoKitError::SpriteFile(path_buf))?,
252        ))
253    }
254
255    /// Finds a sprite that matches the given id! Check out the DefaultIds static class for some built-in ids. Sprites
256    /// will auto-id themselves using this pattern if single sprites: {Tex.Id}/sprite, and this pattern if atlased
257    /// sprites: {Tex.Id}/sprite/atlas/{atlasId}.
258    /// <https://stereokit.net/Pages/StereoKit/Sprite/Find.html>
259    /// * `id` - The id of the sprite to find.
260    ///
261    /// see also [`sprite_find`] [`Sprite::clone_ref`]
262    /// ### Examples
263    /// ```
264    /// # stereokit_rust::test_init_sk!();
265    /// use stereokit_rust::{sprite::{Sprite, SpriteType}, tex::Tex,
266    ///                      util::{Gradient, Color128, named_colors}};
267    ///
268    /// let mut gradient = Gradient::new(None);
269    /// gradient
270    ///     .add(Color128::BLACK_TRANSPARENT, 0.0)
271    ///     .add(Color128::BLACK, 0.7);
272    /// let tex_particule1 = Tex::gen_particle(128, 128, 0.2, Some(gradient));
273    /// let mut sprite = Sprite::from_tex(&tex_particule1, None, None)
274    ///                   .expect("Should be able to create sprite");
275    /// assert!(sprite.get_id().starts_with( "auto/tex_"));
276    ///
277    /// sprite.id("My_sprite_ID");
278    ///
279    /// let same_sprite = Sprite::find("My_sprite_ID")
280    ///                        .expect("Should be able to find sprite");
281    ///
282    /// assert_eq!(same_sprite, sprite);
283    /// ```
284    pub fn find<S: AsRef<str>>(id: S) -> Result<Sprite, StereoKitError> {
285        let cstr_id = CString::new(id.as_ref())?;
286        Ok(Sprite(
287            NonNull::new(unsafe { sprite_find(cstr_id.as_ptr()) })
288                .ok_or(StereoKitError::SpriteFind(id.as_ref().to_string(), "sprite_find failed".to_string()))?,
289        ))
290    }
291
292    /// Creates a clone of the same reference. Basically, the new variable is the same asset. This is what you get by
293    /// calling find() method.
294    /// <https://stereokit.net/Pages/StereoKit/Sprite/Find.html>
295    ///
296    /// see also [`sprite_find()`]
297    /// ### Examples
298    /// ```
299    /// # stereokit_rust::test_init_sk!();
300    /// use stereokit_rust::{sprite::{Sprite, SpriteType}, tex::Tex};
301    ///
302    /// let tex = Tex::rough();
303    /// let mut sprite = Sprite::from_tex(&tex, None, None)
304    ///                   .expect("Should be able to create sprite");
305    /// assert_eq!(sprite.get_id(), "default/tex_rough/sprite");
306    ///
307    /// sprite.id("My_sprite_ID");
308    ///
309    /// let same_sprite = sprite.clone_ref();
310    ///
311    /// assert_eq!(same_sprite, sprite);
312    /// ```
313    pub fn clone_ref(&self) -> Sprite {
314        Sprite(
315            NonNull::new(unsafe { sprite_find(sprite_get_id(self.0.as_ptr())) }).expect("<asset>::clone_ref failed!"),
316        )
317    }
318
319    /// Sets the unique identifier of this asset resource! This can be helpful for debugging,
320    /// managing your assets, or finding them later on!
321    ///<https://stereokit.net/Pages/StereoKit/Sprite/Id.html>
322    ///
323    /// see also [`sprite_get_id`]
324    /// ### Examples
325    /// ```
326    /// # stereokit_rust::test_init_sk!();
327    /// use stereokit_rust::{sprite::{Sprite, SpriteType}, tex::Tex};
328    ///
329    /// let tex = Tex::rough();
330    /// let mut sprite = Sprite::from_tex(&tex, None, None)
331    ///                   .expect("Should be able to create sprite");
332    /// assert_eq!(sprite.get_id(), "default/tex_rough/sprite");
333    ///
334    /// sprite.id("My_sprite_ID");
335    ///
336    /// assert_eq!(sprite.get_id(), "My_sprite_ID");
337    /// ```
338    pub fn id<S: AsRef<str>>(&mut self, id: S) -> &mut Self {
339        let cstr_id = CString::new(id.as_ref()).unwrap();
340        unsafe { sprite_set_id(self.0.as_ptr(), cstr_id.as_ptr()) };
341        self
342    }
343
344    /// Draws the sprite at the location specified by the transform matrix. A sprite is always sized in model space as 1 x Aspect
345    /// meters on the x and y axes respectively, so scale appropriately. The ‘position’ attribute describes what corner of the sprite
346    ///  you’re specifying the transform of.
347    /// <https://stereokit.net/Pages/StereoKit/Sprite/Draw.html>
348    /// * `token` - The token to ensure the sprite is drawn in the correct frame.
349    /// * `transform` - A Matrix describing a transform from model space to world space. A sprite is always sized in
350    ///   model space as 1 x Aspect meters on the x and y axes respectively, so scale appropriately and remember that
351    ///   your anchor position may affect the transform as well.
352    /// * `pivot_position` - Describes what corner of the sprite you’re specifying the transform of. The ‘Pivot’ point
353    ///   or ‘Origin’ of the Sprite.
354    /// * `linear_color` - Per-instance color data for this render item. It is unmodified by StereoKit, and is generally
355    ///   interpreted as linear. If None has default value of WHITE.
356    ///
357    /// see also [`sprite_draw`]
358    /// ### Examples
359    /// ```
360    /// # stereokit_rust::test_init_sk!();
361    /// use stereokit_rust::{ maths::Matrix, sprite::{Sprite, SpriteType}, tex::Tex, system::Pivot,
362    ///                      util::{Gradient, Color128, named_colors}};
363    ///
364    /// let sprite = Sprite::close();
365    ///
366    /// filename_scr = "screenshots/sprite_draw.jpeg";
367    /// test_screenshot!( // !!!! Get a proper main loop !!!!
368    ///     sprite.draw(token, Matrix::Y_180, Pivot::TopLeft,     None);
369    ///     sprite.draw(token, Matrix::Y_180, Pivot::TopRight,    Some(named_colors::BLUE.into()));
370    ///     sprite.draw(token, Matrix::Y_180, Pivot::BottomLeft,  Some(named_colors::RED.into()));
371    ///     sprite.draw(token, Matrix::Y_180, Pivot::BottomRight, Some(named_colors::GREEN.into()));
372    /// );
373    /// ```
374    /// <img src="https://raw.githubusercontent.com/mvvvv/StereoKit-rust/refs/heads/master/screenshots/sprite_draw.jpeg" alt="screenshot" width="200">
375    pub fn draw(
376        &self,
377        _token: &MainThreadToken,
378        transform: impl Into<Matrix>,
379        pivot_position: Pivot,
380        linear_color: Option<Color32>,
381    ) {
382        let color_linear = linear_color.unwrap_or(Color32::WHITE);
383        unsafe { sprite_draw(self.0.as_ptr(), transform.into(), pivot_position, color_linear) };
384    }
385
386    /// The id of this sprite
387    /// <https://stereokit.net/Pages/StereoKit/Sprite/Id.html>
388    ///
389    /// see also [`sprite_get_id`]
390    /// see example in [`Sprite::id`]
391    pub fn get_id(&self) -> &str {
392        unsafe { CStr::from_ptr(sprite_get_id(self.0.as_ptr())) }.to_str().unwrap()
393    }
394
395    /// The aspect ratio of the sprite! This is width/height. You may also be interested in the NormalizedDimensions property,
396    /// which are normalized to the 0-1 range.
397    /// <https://stereokit.net/Pages/StereoKit/Sprite/Aspect.html>
398    ///
399    /// see also [`sprite_get_aspect`]
400    /// ### Examples
401    /// ```
402    /// # stereokit_rust::test_init_sk!();
403    /// use stereokit_rust::sprite::{Sprite, SpriteType};
404    ///
405    /// let sprite = Sprite::from_file("icons/log_viewer.png", Some(SpriteType::Single), Some("MY_ID"))
406    ///                   .expect("open_gltf.jpeg should be able to create sprite");
407    /// assert_eq!(sprite.get_aspect(), 1.0);
408    ///
409    /// let sprite = Sprite::from_file("hdri/sky_dawn.hdr", None, None)
410    ///                  .expect("open_gltf.jpeg should be able to create sprite");
411    /// assert_eq!(sprite.get_aspect(), 2.0);
412    /// ```
413    pub fn get_aspect(&self) -> f32 {
414        unsafe { sprite_get_aspect(self.0.as_ptr()) }
415    }
416
417    /// Get the height in pixel
418    /// <https://stereokit.net/Pages/StereoKit/Sprite/Height.html>
419    ///
420    /// see also [`sprite_get_height`]
421    /// ### Examples
422    /// ```
423    /// # stereokit_rust::test_init_sk!();
424    /// use stereokit_rust::sprite::{Sprite, SpriteType};
425    ///
426    /// let sprite = Sprite::from_file("icons/log_viewer.png", Some(SpriteType::Single), Some("MY_ID"))
427    ///                   .expect("open_gltf.jpeg should be able to create sprite");
428    /// assert_eq!(sprite.get_height(), 128);
429    ///
430    /// let sprite = Sprite::from_file("hdri/sky_dawn.hdr", None, None)
431    ///                  .expect("open_gltf.jpeg should be able to create sprite");
432    /// assert_eq!(sprite.get_height(), 2048);
433    /// ```
434    pub fn get_height(&self) -> i32 {
435        unsafe { sprite_get_height(self.0.as_ptr()) }
436    }
437
438    /// Get the width in pixel
439    /// <https://stereokit.net/Pages/StereoKit/Sprite/Width.html>
440    ///
441    /// see also [`sprite_get_width`]
442    /// ### Examples
443    /// ```
444    /// # stereokit_rust::test_init_sk!();
445    /// use stereokit_rust::sprite::{Sprite, SpriteType};
446    ///
447    /// let sprite = Sprite::from_file("icons/log_viewer.png", Some(SpriteType::Single), Some("MY_ID"))
448    ///                   .expect("open_gltf.jpeg should be able to create sprite");
449    /// assert_eq!(sprite.get_width(), 128);
450    ///
451    /// let sprite = Sprite::from_file("hdri/sky_dawn.hdr", None, None)
452    ///                  .expect("open_gltf.jpeg should be able to create sprite");
453    /// assert_eq!(sprite.get_width(), 4096);
454    /// ```
455    pub fn get_width(&self) -> i32 {
456        unsafe { sprite_get_width(self.0.as_ptr()) }
457    }
458
459    /// Get the width and height of the sprite, normalized so the maximum value is 1.
460    /// <https://stereokit.net/Pages/StereoKit/Sprite/NormalizedDimensions.html>
461    ///
462    /// see also [`sprite_get_dimensions_normalized`]
463    /// ### Examples
464    /// ```
465    /// # stereokit_rust::test_init_sk!();
466    /// use stereokit_rust::{sprite::{Sprite, SpriteType}, maths::Vec2};
467    ///
468    /// let sprite = Sprite::from_file("icons/log_viewer.png", Some(SpriteType::Single), Some("MY_ID"))
469    ///                   .expect("open_gltf.jpeg should be able to create sprite");
470    /// assert_eq!(sprite.get_normalized_dimensions(), Vec2::ONE);
471    ///
472    /// let sprite = Sprite::from_file("hdri/sky_dawn.hdr", None, None)
473    ///                  .expect("open_gltf.jpeg should be able to create sprite");
474    /// assert_eq!(sprite.get_normalized_dimensions(), Vec2::new(1.0, 0.5));
475    /// ```
476    pub fn get_normalized_dimensions(&self) -> Vec2 {
477        unsafe { sprite_get_dimensions_normalized(self.0.as_ptr()) }
478    }
479
480    /// This is a 64x64 image of a filled hole. This is common iconography for radio buttons which use an empty hole to
481    /// indicate an un-selected radio, and a filled hole for a selected radio. This is used by the UI for radio buttons!
482    /// <https://stereokit.net/Pages/StereoKit/Sprite/RadioOn.html>
483    ///
484    /// ### Examples
485    /// ```
486    /// # stereokit_rust::test_init_sk!();
487    /// use stereokit_rust::{sprite::Sprite, maths::Matrix, system::Pivot};
488    ///
489    /// let sprite = Sprite::radio_on();
490    /// assert_eq!(sprite.get_id(), "sk/ui/radio_on");
491    ///
492    /// width_scr = 48; height_scr = 48; fov_scr = 65.0;
493    /// filename_scr = "screenshots/sprite_radio_on.jpeg";
494    /// test_screenshot!( // !!!! Get a proper main loop !!!!
495    ///     sprite.draw(token, Matrix::Y_180, Pivot::Center, None);
496    /// );
497    /// ```
498    /// <img src="https://raw.githubusercontent.com/mvvvv/StereoKit-rust/refs/heads/master/screenshots/sprite_radio_on.jpeg" alt="screenshot" width="48">
499    pub fn radio_on() -> Self {
500        let cstr_id = CString::new("sk/ui/radio_on").unwrap();
501        Sprite(NonNull::new(unsafe { sprite_find(cstr_id.as_ptr()) }).unwrap())
502    }
503
504    /// This is a 64x64 image of an empty hole. This is common iconography for radio buttons which use an empty hole to
505    /// indicate an un-selected radio, and a filled hole for a selected radio. This is used by the UI for radio buttons!
506    /// <https://stereokit.net/Pages/StereoKit/Sprite/RadioOff.html>
507    ///
508    /// ### Examples
509    /// ```
510    /// # stereokit_rust::test_init_sk!();
511    /// use stereokit_rust::{sprite::Sprite, maths::Matrix, system::Pivot};
512    ///
513    /// let sprite = Sprite::radio_off();
514    /// assert_eq!(sprite.get_id(), "sk/ui/radio_off");
515    ///
516    /// width_scr = 48; height_scr = 48; fov_scr = 65.0;
517    /// filename_scr = "screenshots/sprite_radio_off.jpeg";
518    /// test_screenshot!( // !!!! Get a proper main loop !!!!
519    ///     sprite.draw(token, Matrix::Y_180, Pivot::Center, None);
520    /// );
521    /// ```
522    /// <img src="https://raw.githubusercontent.com/mvvvv/StereoKit-rust/refs/heads/master/screenshots/sprite_radio_off.jpeg" alt="screenshot" width="48">
523    pub fn radio_off() -> Self {
524        let cstr_id = CString::new("sk/ui/radio_off").unwrap();
525        Sprite(NonNull::new(unsafe { sprite_find(cstr_id.as_ptr()) }).unwrap())
526    }
527
528    /// This is a 64x64 image of a filled rounded square. This is common iconography for checkboxes which use an
529    /// empty square to indicate an un-selected checkbox, and a filled square for a selected checkbox. This is used
530    /// by the UI for toggle buttons!
531    /// <https://stereokit.net/Pages/StereoKit/Sprite/ToggleOn.html>
532    ///
533    /// ### Examples
534    /// ```
535    /// # stereokit_rust::test_init_sk!();
536    /// use stereokit_rust::{sprite::Sprite, maths::Matrix, system::Pivot};
537    ///
538    /// let sprite = Sprite::toggle_on();
539    /// assert_eq!(sprite.get_id(), "sk/ui/toggle_on");
540    ///
541    /// width_scr = 48; height_scr = 48; fov_scr = 65.0;
542    /// filename_scr = "screenshots/sprite_toggle_on.jpeg";
543    /// test_screenshot!( // !!!! Get a proper main loop !!!!
544    ///     sprite.draw(token, Matrix::Y_180, Pivot::Center, None);
545    /// );
546    /// ```
547    /// <img src="https://raw.githubusercontent.com/mvvvv/StereoKit-rust/refs/heads/master/screenshots/sprite_toggle_on.jpeg" alt="screenshot" width="48">
548    pub fn toggle_on() -> Self {
549        let cstr_id = CString::new("sk/ui/toggle_on").unwrap();
550        Sprite(NonNull::new(unsafe { sprite_find(cstr_id.as_ptr()) }).unwrap())
551    }
552
553    /// This is a 64x64 image of an empty rounded square. This is common iconography for checkboxes which use an empty
554    /// square to indicate an un-selected checkbox, and a filled square for a selected checkbox. This is used by the UI
555    /// for toggle buttons!
556    /// <https://stereokit.net/Pages/StereoKit/Sprite/ToggleOff.html>
557    ///
558    /// ### Examples
559    /// ```
560    /// # stereokit_rust::test_init_sk!();
561    /// use stereokit_rust::{sprite::Sprite, maths::Matrix, system::Pivot};
562    ///
563    /// let sprite = Sprite::toggle_off();
564    /// assert_eq!(sprite.get_id(), "sk/ui/toggle_off");
565    ///
566    /// width_scr = 48; height_scr = 48; fov_scr = 65.0;
567    /// filename_scr = "screenshots/sprite_toggle_off.jpeg";
568    /// test_screenshot!( // !!!! Get a proper main loop !!!!
569    ///     sprite.draw(token, Matrix::Y_180, Pivot::Center, None);
570    /// );
571    /// ```
572    /// <img src="https://raw.githubusercontent.com/mvvvv/StereoKit-rust/refs/heads/master/screenshots/sprite_toggle_off.jpeg" alt="screenshot" width="48">
573    pub fn toggle_off() -> Self {
574        let cstr_id = CString::new("sk/ui/toggle_off").unwrap();
575        Sprite(NonNull::new(unsafe { sprite_find(cstr_id.as_ptr()) }).unwrap())
576    }
577
578    /// This is a 64x64 image of a slightly rounded triangle pointing left.
579    /// <https://stereokit.net/Pages/StereoKit/Sprite/ArrowLeft.html>
580    ///
581    /// ### Examples
582    /// ```
583    /// # stereokit_rust::test_init_sk!();
584    /// use stereokit_rust::{sprite::Sprite, maths::{Vec3, Matrix}, system::Pivot};
585    ///
586    /// let sprite = Sprite::arrow_left();
587    /// assert_eq!(sprite.get_id(), "sk/ui/arrow_left");
588    ///
589    /// width_scr = 48; height_scr = 48; fov_scr = 65.0;
590    /// filename_scr = "screenshots/sprite_arrow_left.jpeg";
591    /// test_screenshot!( // !!!! Get a proper main loop !!!!
592    ///     sprite.draw(token, Matrix::Y_180, Pivot::Center, None);
593    /// );
594    /// ```
595    /// <img src="https://raw.githubusercontent.com/mvvvv/StereoKit-rust/refs/heads/master/screenshots/sprite_arrow_left.jpeg" alt="screenshot" width="48">
596    pub fn arrow_left() -> Self {
597        let cstr_id = CString::new("sk/ui/arrow_left").unwrap();
598        Sprite(NonNull::new(unsafe { sprite_find(cstr_id.as_ptr()) }).unwrap())
599    }
600
601    /// This is a 64x64 image of a slightly rounded triangle pointing right.
602    /// <https://stereokit.net/Pages/StereoKit/Sprite/ArrowRight.html>
603    ///
604    /// ### Examples
605    /// ```
606    /// # stereokit_rust::test_init_sk!();
607    /// use stereokit_rust::{sprite::Sprite, maths::{Vec3, Matrix}, system::Pivot};
608    ///
609    /// let sprite = Sprite::arrow_right();
610    /// assert_eq!(sprite.get_id(), "sk/ui/arrow_right");
611    ///
612    /// width_scr = 48; height_scr = 48; fov_scr = 65.0;
613    /// filename_scr = "screenshots/sprite_arrow_right.jpeg";
614    /// test_screenshot!( // !!!! Get a proper main loop !!!!
615    ///     sprite.draw(token, Matrix::Y_180, Pivot::Center, None);
616    /// );
617    /// ```
618    /// <img src="https://raw.githubusercontent.com/mvvvv/StereoKit-rust/refs/heads/master/screenshots/sprite_arrow_right.jpeg" alt="screenshot" width="48">
619    pub fn arrow_right() -> Self {
620        let cstr_id = CString::new("sk/ui/arrow_right").unwrap();
621        Sprite(NonNull::new(unsafe { sprite_find(cstr_id.as_ptr()) }).unwrap())
622    }
623
624    /// This is a 64x64 image of a slightly rounded triangle pointing up.
625    /// <https://stereokit.net/Pages/StereoKit/Sprite/ArrowUp.html>
626    ///
627    /// ### Examples
628    /// ```
629    /// # stereokit_rust::test_init_sk!();
630    /// use stereokit_rust::{sprite::Sprite, maths::Matrix, system::Pivot};
631    ///
632    /// let sprite = Sprite::arrow_up();
633    /// assert_eq!(sprite.get_id(), "sk/ui/arrow_up");
634    ///
635    /// width_scr = 48; height_scr = 48; fov_scr = 65.0;
636    /// filename_scr = "screenshots/sprite_arrow_up.jpeg";
637    /// test_screenshot!( // !!!! Get a proper main loop !!!!
638    ///     sprite.draw(token, Matrix::Y_180, Pivot::Center, None);
639    /// );
640    /// ```
641    /// <img src="https://raw.githubusercontent.com/mvvvv/StereoKit-rust/refs/heads/master/screenshots/sprite_arrow_up.jpeg" alt="screenshot" width="48">
642    pub fn arrow_up() -> Self {
643        let cstr_id = CString::new("sk/ui/arrow_up").unwrap();
644        Sprite(NonNull::new(unsafe { sprite_find(cstr_id.as_ptr()) }).unwrap())
645    }
646
647    /// This is a 64x64 image of a slightly rounded triangle pointing down.
648    /// <https://stereokit.net/Pages/StereoKit/Sprite/ArrowDown.html>
649    ///
650    /// ### Examples
651    /// ```
652    /// # stereokit_rust::test_init_sk!();
653    /// use stereokit_rust::{sprite::Sprite, maths::Matrix, system::Pivot};
654    ///
655    /// let sprite = Sprite::arrow_down();
656    /// assert_eq!(sprite.get_id(), "sk/ui/arrow_down");
657    ///
658    /// width_scr = 48; height_scr = 48; fov_scr = 65.0;
659    /// filename_scr = "screenshots/sprite_arrow_down.jpeg";
660    /// test_screenshot!( // !!!! Get a proper main loop !!!!
661    ///     sprite.draw(token, Matrix::Y_180, Pivot::Center, None);
662    /// );
663    /// ```
664    /// <img src="https://raw.githubusercontent.com/mvvvv/StereoKit-rust/refs/heads/master/screenshots/sprite_arrow_down.jpeg" alt="screenshot" width="48">
665    pub fn arrow_down() -> Self {
666        let cstr_id = CString::new("sk/ui/arrow_down").unwrap();
667        Sprite(NonNull::new(unsafe { sprite_find(cstr_id.as_ptr()) }).unwrap())
668    }
669
670    /// This is a 64x64 image of a backspace action button, similar to a backspace button you might find on a mobile
671    /// keyboard.
672    /// <https://stereokit.net/Pages/StereoKit/Sprite/Backspace.html>
673    ///
674    /// ### Examples
675    /// ```
676    /// # stereokit_rust::test_init_sk!();
677    /// use stereokit_rust::{sprite::Sprite, maths::Matrix, system::Pivot};
678    ///
679    /// let sprite = Sprite::backspace();
680    /// assert_eq!(sprite.get_id(), "sk/ui/backspace");
681    ///
682    /// width_scr = 48; height_scr = 48; fov_scr = 65.0;
683    /// filename_scr = "screenshots/sprite_backspace.jpeg";
684    /// test_screenshot!( // !!!! Get a proper main loop !!!!
685    ///     sprite.draw(token, Matrix::Y_180, Pivot::Center, None);
686    /// );
687    /// ```
688    /// <img src="https://raw.githubusercontent.com/mvvvv/StereoKit-rust/refs/heads/master/screenshots/sprite_backspace.jpeg" alt="screenshot" width="48">
689    pub fn backspace() -> Self {
690        let cstr_id = CString::new("sk/ui/backspace").unwrap();
691        Sprite(NonNull::new(unsafe { sprite_find(cstr_id.as_ptr()) }).unwrap())
692    }
693
694    /// This is a 64x64 image of an upward facing rounded arrow. This is a triangular top with a narrow rectangular
695    /// base, and is used to indicate a ‘shift’ icon on a keyboard.
696    /// <https://stereokit.net/Pages/StereoKit/Sprite/Shift.html>
697    ///
698    /// ### Examples
699    /// ```
700    /// # stereokit_rust::test_init_sk!();
701    /// use stereokit_rust::{sprite::Sprite, maths::Matrix, system::Pivot};
702    ///
703    /// let sprite = Sprite::shift();
704    /// assert_eq!(sprite.get_id(), "sk/ui/shift");
705    ///
706    /// width_scr = 48; height_scr = 48; fov_scr = 65.0;
707    /// filename_scr = "screenshots/sprite_shift.jpeg";
708    /// test_screenshot!( // !!!! Get a proper main loop !!!!
709    ///     sprite.draw(token, Matrix::Y_180, Pivot::Center, None);
710    /// );
711    /// ```
712    /// <img src="https://raw.githubusercontent.com/mvvvv/StereoKit-rust/refs/heads/master/screenshots/sprite_shift.jpeg" alt="screenshot" width="48">
713    pub fn shift() -> Self {
714        let cstr_id = CString::new("sk/ui/shift").unwrap();
715        Sprite(NonNull::new(unsafe { sprite_find(cstr_id.as_ptr()) }).unwrap())
716    }
717
718    /// This is a 64x64 image of a square aspect X, with rounded edge. It’s used to indicate a ‘close’ icon.
719    /// <https://stereokit.net/Pages/StereoKit/Sprite/Close.html>
720    ///
721    /// ### Examples
722    /// ```
723    /// # stereokit_rust::test_init_sk!();
724    /// use stereokit_rust::{sprite::Sprite, maths::Matrix, system::Pivot};
725    ///
726    /// let sprite = Sprite::close();
727    /// assert_eq!(sprite.get_id(), "sk/ui/close");
728    ///
729    /// width_scr = 48; height_scr = 48; fov_scr = 65.0;
730    /// filename_scr = "screenshots/sprite_close.jpeg";
731    /// test_screenshot!( // !!!! Get a proper main loop !!!!
732    ///     sprite.draw(token, Matrix::Y_180, Pivot::Center, None);
733    /// );
734    /// ```
735    /// <img src="https://raw.githubusercontent.com/mvvvv/StereoKit-rust/refs/heads/master/screenshots/sprite_close.jpeg" alt="screenshot" width="48">
736    pub fn close() -> Self {
737        let cstr_id = CString::new("sk/ui/close").unwrap();
738        Sprite(NonNull::new(unsafe { sprite_find(cstr_id.as_ptr()) }).unwrap())
739    }
740
741    /// <https://stereokit.net/Pages/StereoKit/Sprite/List.html>
742    ///
743    /// ### Examples
744    /// ```
745    /// # stereokit_rust::test_init_sk!();
746    /// use stereokit_rust::{sprite::Sprite, maths::Matrix, system::Pivot};
747    ///
748    /// let sprite = Sprite::list();
749    /// assert_eq!(sprite.get_id(), "sk/ui/list");
750    ///
751    /// width_scr = 48; height_scr = 48; fov_scr = 65.0;
752    /// filename_scr = "screenshots/sprite_list.jpeg";
753    /// test_screenshot!( // !!!! Get a proper main loop !!!!
754    ///     sprite.draw(token, Matrix::Y_180, Pivot::Center, None);
755    /// );
756    /// ```
757    /// <img src="https://raw.githubusercontent.com/mvvvv/StereoKit-rust/refs/heads/master/screenshots/sprite_list.jpeg" alt="screenshot" width="48">
758    pub fn list() -> Self {
759        let cstr_id = CString::new("sk/ui/list").unwrap();
760        Sprite(NonNull::new(unsafe { sprite_find(cstr_id.as_ptr()) }).unwrap())
761    }
762
763    /// <https://stereokit.net/Pages/StereoKit/Sprite/Grid.html>
764    ///
765    /// ### Examples
766    /// ```
767    /// # stereokit_rust::test_init_sk!();
768    /// use stereokit_rust::{sprite::Sprite, maths::Matrix, system::Pivot};
769    ///
770    /// let sprite = Sprite::grid();
771    /// assert_eq!(sprite.get_id(), "sk/ui/grid");
772    ///
773    /// width_scr = 48; height_scr = 48; fov_scr = 65.0;
774    /// filename_scr = "screenshots/sprite_grid.jpeg";
775    /// test_screenshot!( // !!!! Get a proper main loop !!!!
776    ///     sprite.draw(token, Matrix::Y_180, Pivot::Center, None);
777    /// );
778    /// ```
779    /// <img src="https://raw.githubusercontent.com/mvvvv/StereoKit-rust/refs/heads/master/screenshots/sprite_grid.jpeg" alt="screenshot" width="48">
780    pub fn grid() -> Self {
781        let cstr_id = CString::new("sk/ui/grid").unwrap();
782        Sprite(NonNull::new(unsafe { sprite_find(cstr_id.as_ptr()) }).unwrap())
783    }
784}