Skip to main content

goud_engine/ecs/components/
sprite.rs

1//! Sprite component for 2D rendering.
2//!
3//! This module provides the [`Sprite`] component for rendering 2D textured quads.
4//! Sprites are the fundamental building block for 2D games, representing a
5//! rectangular image that can be positioned, rotated, scaled, and tinted.
6//!
7//! # Component Structure
8//!
9//! A [`Sprite`] component contains:
10//! - **Texture reference**: [`AssetHandle<TextureAsset>`] pointing to the image data
11//! - **Color tint**: RGBA color multiplied with texture colors
12//! - **Source rectangle**: Optional UV rect for sprite sheets and atlases
13//! - **Flip flags**: Horizontal and vertical mirroring
14//! - **Anchor point**: Origin point for rotation and positioning (normalized 0-1)
15//! - **Size override**: Optional custom size (defaults to texture size)
16//!
17//! # Usage with ECS
18//!
19//! ```
20//! use goud_engine::ecs::{World, Component};
21//! use goud_engine::ecs::components::{Sprite, Transform2D};
22//! use goud_engine::assets::{AssetServer, loaders::TextureAsset};
23//! use goud_engine::core::math::{Vec2, Color, Rect};
24//!
25//! let mut world = World::new();
26//! let mut asset_server = AssetServer::new();
27//!
28//! // Load texture
29//! let texture = asset_server.load::<TextureAsset>("player.png");
30//!
31//! // Create sprite entity
32//! let entity = world.spawn_empty();
33//! world.insert(entity, Transform2D::from_position(Vec2::new(100.0, 100.0)));
34//! world.insert(entity, Sprite::new(texture));
35//! ```
36//!
37//! # Sprite Sheets and Atlases
38//!
39//! Use `with_source_rect()` to render a portion of a texture:
40//!
41//! ```
42//! use goud_engine::ecs::components::Sprite;
43//! use goud_engine::core::math::Rect;
44//! # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
45//! # let mut asset_server = AssetServer::new();
46//! # let texture = asset_server.load::<TextureAsset>("spritesheet.png");
47//!
48//! let sprite = Sprite::new(texture)
49//!     .with_source_rect(Rect::new(0.0, 0.0, 32.0, 32.0)) // Top-left 32x32 tile
50//!     .with_anchor(0.5, 0.5); // Center anchor
51//! ```
52//!
53//! # Color Tinting
54//!
55//! Sprites can be tinted by multiplying the texture color with a color value:
56//!
57//! ```
58//! use goud_engine::ecs::components::Sprite;
59//! use goud_engine::core::math::Color;
60//! # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
61//! # let mut asset_server = AssetServer::new();
62//! # let texture = asset_server.load::<TextureAsset>("player.png");
63//!
64//! // Red tint with 50% transparency
65//! let sprite = Sprite::new(texture)
66//!     .with_color(Color::rgba(1.0, 0.0, 0.0, 0.5));
67//! ```
68//!
69//! # Flipping
70//!
71//! Sprites can be flipped horizontally or vertically:
72//!
73//! ```
74//! use goud_engine::ecs::components::Sprite;
75//! # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
76//! # let mut asset_server = AssetServer::new();
77//! # let texture = asset_server.load::<TextureAsset>("player.png");
78//!
79//! let sprite = Sprite::new(texture)
80//!     .with_flip_x(true)  // Mirror horizontally
81//!     .with_flip_y(false);
82//! ```
83//!
84//! # Anchor Points
85//!
86//! The anchor point determines the origin for rotation and positioning.
87//! Coordinates are normalized (0.0 - 1.0):
88//!
89//! - `(0.0, 0.0)` = Top-left corner
90//! - `(0.5, 0.5)` = Center (default)
91//! - `(1.0, 1.0)` = Bottom-right corner
92//!
93//! ```
94//! use goud_engine::ecs::components::Sprite;
95//! # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
96//! # let mut asset_server = AssetServer::new();
97//! # let texture = asset_server.load::<TextureAsset>("player.png");
98//!
99//! let sprite = Sprite::new(texture)
100//!     .with_anchor(0.5, 1.0); // Bottom-center anchor
101//! ```
102//!
103//! # Custom Size
104//!
105//! By default, sprites render at their texture's pixel size. You can override this:
106//!
107//! ```
108//! use goud_engine::ecs::components::Sprite;
109//! use goud_engine::core::math::Vec2;
110//! # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
111//! # let mut asset_server = AssetServer::new();
112//! # let texture = asset_server.load::<TextureAsset>("player.png");
113//!
114//! let sprite = Sprite::new(texture)
115//!     .with_custom_size(Vec2::new(64.0, 64.0)); // Force 64x64 size
116//! ```
117
118use crate::assets::{loaders::TextureAsset, AssetHandle};
119use crate::core::math::{Color, Rect, Vec2};
120use crate::ecs::Component;
121
122// =============================================================================
123// Sprite Component
124// =============================================================================
125
126/// A 2D sprite component for rendering textured quads.
127///
128/// The `Sprite` component defines how a texture should be rendered in 2D space.
129/// It must be paired with a [`Transform2D`](crate::ecs::components::Transform2D)
130/// or [`GlobalTransform2D`](crate::ecs::components::GlobalTransform2D) component
131/// to define the sprite's position, rotation, and scale.
132///
133/// # Fields
134///
135/// - `texture`: Handle to the texture asset to render
136/// - `color`: Color tint multiplied with texture pixels (default: white)
137/// - `source_rect`: Optional UV rectangle for sprite sheets (default: full texture)
138/// - `flip_x`: Flip the sprite horizontally (default: false)
139/// - `flip_y`: Flip the sprite vertically (default: false)
140/// - `anchor`: Normalized anchor point for rotation/positioning (default: center)
141/// - `custom_size`: Optional override for sprite size (default: texture size)
142///
143/// # Examples
144///
145/// ## Basic Sprite
146///
147/// ```
148/// use goud_engine::ecs::components::Sprite;
149/// use goud_engine::assets::{AssetServer, loaders::TextureAsset};
150///
151/// let mut asset_server = AssetServer::new();
152/// let texture = asset_server.load::<TextureAsset>("player.png");
153///
154/// let sprite = Sprite::new(texture);
155/// ```
156///
157/// ## Sprite with Custom Properties
158///
159/// ```
160/// use goud_engine::ecs::components::Sprite;
161/// use goud_engine::core::math::{Color, Vec2};
162/// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
163/// # let mut asset_server = AssetServer::new();
164/// # let texture = asset_server.load::<TextureAsset>("player.png");
165///
166/// let sprite = Sprite::new(texture)
167///     .with_color(Color::rgba(1.0, 0.5, 0.5, 0.8))
168///     .with_flip_x(true)
169///     .with_anchor(0.5, 1.0)
170///     .with_custom_size(Vec2::new(64.0, 64.0));
171/// ```
172///
173/// ## Sprite Sheet Frame
174///
175/// ```
176/// use goud_engine::ecs::components::Sprite;
177/// use goud_engine::core::math::Rect;
178/// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
179/// # let mut asset_server = AssetServer::new();
180/// # let texture = asset_server.load::<TextureAsset>("spritesheet.png");
181///
182/// // Extract a 32x32 tile from the sprite sheet
183/// let sprite = Sprite::new(texture)
184///     .with_source_rect(Rect::new(64.0, 32.0, 32.0, 32.0));
185/// ```
186#[derive(Debug, Clone, PartialEq)]
187pub struct Sprite {
188    /// Handle to the texture asset to render.
189    pub texture: AssetHandle<TextureAsset>,
190
191    /// Color tint multiplied with texture pixels.
192    ///
193    /// Each component is in range [0.0, 1.0]. White (1, 1, 1, 1) renders
194    /// the texture unmodified. RGB values tint the color, alpha controls
195    /// transparency.
196    pub color: Color,
197
198    /// Optional source rectangle for sprite sheets and atlases.
199    ///
200    /// If `None`, the entire texture is rendered. If `Some`, only the
201    /// specified rectangle (in pixel coordinates) is rendered.
202    ///
203    /// For normalized UV coordinates, multiply by texture dimensions.
204    pub source_rect: Option<Rect>,
205
206    /// Flip the sprite horizontally.
207    ///
208    /// When true, the texture is mirrored along the Y-axis.
209    pub flip_x: bool,
210
211    /// Flip the sprite vertically.
212    ///
213    /// When true, the texture is mirrored along the X-axis.
214    pub flip_y: bool,
215
216    /// Normalized anchor point for rotation and positioning.
217    ///
218    /// Coordinates are in range [0.0, 1.0]:
219    /// - `(0.0, 0.0)` = Top-left corner
220    /// - `(0.5, 0.5)` = Center (default)
221    /// - `(1.0, 1.0)` = Bottom-right corner
222    ///
223    /// The anchor point is the origin for rotation and the point that aligns
224    /// with the entity's Transform2D position.
225    pub anchor: Vec2,
226
227    /// Optional custom size override.
228    ///
229    /// If `None`, the sprite renders at the texture's pixel dimensions
230    /// (or source_rect dimensions if specified). If `Some`, the sprite
231    /// is scaled to this size.
232    pub custom_size: Option<Vec2>,
233}
234
235impl Sprite {
236    /// Creates a new sprite with default settings.
237    ///
238    /// The sprite will render the entire texture with:
239    /// - White color tint (no modification)
240    /// - No source rectangle (full texture)
241    /// - No flipping
242    /// - Center anchor point (0.5, 0.5)
243    /// - No custom size (uses texture dimensions)
244    ///
245    /// # Example
246    ///
247    /// ```
248    /// use goud_engine::ecs::components::Sprite;
249    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
250    /// # let mut asset_server = AssetServer::new();
251    /// # let texture = asset_server.load::<TextureAsset>("player.png");
252    ///
253    /// let sprite = Sprite::new(texture);
254    /// ```
255    #[inline]
256    pub fn new(texture: AssetHandle<TextureAsset>) -> Self {
257        Self {
258            texture,
259            color: Color::WHITE,
260            source_rect: None,
261            flip_x: false,
262            flip_y: false,
263            anchor: Vec2::new(0.5, 0.5),
264            custom_size: None,
265        }
266    }
267
268    /// Sets the color tint for this sprite.
269    ///
270    /// The color is multiplied with each texture pixel. Use white (1, 1, 1, 1)
271    /// for no tinting.
272    ///
273    /// # Example
274    ///
275    /// ```
276    /// use goud_engine::ecs::components::Sprite;
277    /// use goud_engine::core::math::Color;
278    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
279    /// # let mut asset_server = AssetServer::new();
280    /// # let texture = asset_server.load::<TextureAsset>("player.png");
281    ///
282    /// let sprite = Sprite::new(texture)
283    ///     .with_color(Color::rgba(1.0, 0.0, 0.0, 0.5)); // Red, 50% transparent
284    /// ```
285    #[inline]
286    pub fn with_color(mut self, color: Color) -> Self {
287        self.color = color;
288        self
289    }
290
291    /// Sets the source rectangle for sprite sheet rendering.
292    ///
293    /// The rectangle is specified in pixel coordinates relative to the
294    /// top-left corner of the texture.
295    ///
296    /// # Example
297    ///
298    /// ```
299    /// use goud_engine::ecs::components::Sprite;
300    /// use goud_engine::core::math::Rect;
301    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
302    /// # let mut asset_server = AssetServer::new();
303    /// # let texture = asset_server.load::<TextureAsset>("spritesheet.png");
304    ///
305    /// // Extract a 32x32 tile at position (64, 32)
306    /// let sprite = Sprite::new(texture)
307    ///     .with_source_rect(Rect::new(64.0, 32.0, 32.0, 32.0));
308    /// ```
309    #[inline]
310    pub fn with_source_rect(mut self, rect: Rect) -> Self {
311        self.source_rect = Some(rect);
312        self
313    }
314
315    /// Removes the source rectangle, rendering the full texture.
316    ///
317    /// # Example
318    ///
319    /// ```
320    /// # use goud_engine::ecs::components::Sprite;
321    /// # use goud_engine::core::math::Rect;
322    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
323    /// # let mut asset_server = AssetServer::new();
324    /// # let texture = asset_server.load::<TextureAsset>("spritesheet.png");
325    /// let mut sprite = Sprite::new(texture)
326    ///     .with_source_rect(Rect::new(0.0, 0.0, 32.0, 32.0));
327    ///
328    /// sprite = sprite.without_source_rect();
329    /// assert!(sprite.source_rect.is_none());
330    /// ```
331    #[inline]
332    pub fn without_source_rect(mut self) -> Self {
333        self.source_rect = None;
334        self
335    }
336
337    /// Sets the horizontal flip flag.
338    ///
339    /// When true, the sprite is mirrored along the Y-axis.
340    ///
341    /// # Example
342    ///
343    /// ```
344    /// use goud_engine::ecs::components::Sprite;
345    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
346    /// # let mut asset_server = AssetServer::new();
347    /// # let texture = asset_server.load::<TextureAsset>("player.png");
348    ///
349    /// let sprite = Sprite::new(texture).with_flip_x(true);
350    /// ```
351    #[inline]
352    pub fn with_flip_x(mut self, flip: bool) -> Self {
353        self.flip_x = flip;
354        self
355    }
356
357    /// Sets the vertical flip flag.
358    ///
359    /// When true, the sprite is mirrored along the X-axis.
360    ///
361    /// # Example
362    ///
363    /// ```
364    /// use goud_engine::ecs::components::Sprite;
365    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
366    /// # let mut asset_server = AssetServer::new();
367    /// # let texture = asset_server.load::<TextureAsset>("player.png");
368    ///
369    /// let sprite = Sprite::new(texture).with_flip_y(true);
370    /// ```
371    #[inline]
372    pub fn with_flip_y(mut self, flip: bool) -> Self {
373        self.flip_y = flip;
374        self
375    }
376
377    /// Sets both flip flags at once.
378    ///
379    /// # Example
380    ///
381    /// ```
382    /// use goud_engine::ecs::components::Sprite;
383    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
384    /// # let mut asset_server = AssetServer::new();
385    /// # let texture = asset_server.load::<TextureAsset>("player.png");
386    ///
387    /// let sprite = Sprite::new(texture).with_flip(true, true);
388    /// ```
389    #[inline]
390    pub fn with_flip(mut self, flip_x: bool, flip_y: bool) -> Self {
391        self.flip_x = flip_x;
392        self.flip_y = flip_y;
393        self
394    }
395
396    /// Sets the anchor point with individual coordinates.
397    ///
398    /// Coordinates are normalized in range [0.0, 1.0]:
399    /// - `(0.0, 0.0)` = Top-left
400    /// - `(0.5, 0.5)` = Center
401    /// - `(1.0, 1.0)` = Bottom-right
402    ///
403    /// # Example
404    ///
405    /// ```
406    /// use goud_engine::ecs::components::Sprite;
407    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
408    /// # let mut asset_server = AssetServer::new();
409    /// # let texture = asset_server.load::<TextureAsset>("player.png");
410    ///
411    /// // Bottom-center anchor for ground-aligned sprites
412    /// let sprite = Sprite::new(texture).with_anchor(0.5, 1.0);
413    /// ```
414    #[inline]
415    pub fn with_anchor(mut self, x: f32, y: f32) -> Self {
416        self.anchor = Vec2::new(x, y);
417        self
418    }
419
420    /// Sets the anchor point from a Vec2.
421    ///
422    /// # Example
423    ///
424    /// ```
425    /// use goud_engine::ecs::components::Sprite;
426    /// use goud_engine::core::math::Vec2;
427    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
428    /// # let mut asset_server = AssetServer::new();
429    /// # let texture = asset_server.load::<TextureAsset>("player.png");
430    ///
431    /// let sprite = Sprite::new(texture)
432    ///     .with_anchor_vec(Vec2::new(0.5, 1.0));
433    /// ```
434    #[inline]
435    pub fn with_anchor_vec(mut self, anchor: Vec2) -> Self {
436        self.anchor = anchor;
437        self
438    }
439
440    /// Sets a custom size for the sprite.
441    ///
442    /// When set, the sprite is scaled to this size regardless of the
443    /// texture dimensions.
444    ///
445    /// # Example
446    ///
447    /// ```
448    /// use goud_engine::ecs::components::Sprite;
449    /// use goud_engine::core::math::Vec2;
450    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
451    /// # let mut asset_server = AssetServer::new();
452    /// # let texture = asset_server.load::<TextureAsset>("player.png");
453    ///
454    /// // Force sprite to 64x64 size
455    /// let sprite = Sprite::new(texture)
456    ///     .with_custom_size(Vec2::new(64.0, 64.0));
457    /// ```
458    #[inline]
459    pub fn with_custom_size(mut self, size: Vec2) -> Self {
460        self.custom_size = Some(size);
461        self
462    }
463
464    /// Removes the custom size, using texture dimensions.
465    ///
466    /// # Example
467    ///
468    /// ```
469    /// use goud_engine::ecs::components::Sprite;
470    /// use goud_engine::core::math::Vec2;
471    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
472    /// # let mut asset_server = AssetServer::new();
473    /// # let texture = asset_server.load::<TextureAsset>("player.png");
474    ///
475    /// let mut sprite = Sprite::new(texture)
476    ///     .with_custom_size(Vec2::new(64.0, 64.0));
477    ///
478    /// sprite = sprite.without_custom_size();
479    /// assert!(sprite.custom_size.is_none());
480    /// ```
481    #[inline]
482    pub fn without_custom_size(mut self) -> Self {
483        self.custom_size = None;
484        self
485    }
486
487    /// Gets the effective size of the sprite.
488    ///
489    /// Returns the custom size if set, otherwise the source rect size if set,
490    /// otherwise falls back to a default size (requires texture dimensions).
491    ///
492    /// For actual rendering, you'll need to query the texture asset to get
493    /// its dimensions when custom_size and source_rect are both None.
494    ///
495    /// # Example
496    ///
497    /// ```
498    /// use goud_engine::ecs::components::Sprite;
499    /// use goud_engine::core::math::{Vec2, Rect};
500    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
501    /// # let mut asset_server = AssetServer::new();
502    /// # let texture = asset_server.load::<TextureAsset>("player.png");
503    ///
504    /// let sprite = Sprite::new(texture)
505    ///     .with_source_rect(Rect::new(0.0, 0.0, 32.0, 32.0));
506    ///
507    /// let size = sprite.size_or_rect();
508    /// assert_eq!(size, Vec2::new(32.0, 32.0));
509    /// ```
510    #[inline]
511    pub fn size_or_rect(&self) -> Vec2 {
512        if let Some(size) = self.custom_size {
513            size
514        } else if let Some(rect) = self.source_rect {
515            Vec2::new(rect.width, rect.height)
516        } else {
517            Vec2::zero() // Caller must query texture dimensions
518        }
519    }
520
521    /// Returns true if the sprite has a source rectangle set.
522    #[inline]
523    pub fn has_source_rect(&self) -> bool {
524        self.source_rect.is_some()
525    }
526
527    /// Returns true if the sprite has a custom size set.
528    #[inline]
529    pub fn has_custom_size(&self) -> bool {
530        self.custom_size.is_some()
531    }
532
533    /// Returns true if the sprite is flipped on either axis.
534    #[inline]
535    pub fn is_flipped(&self) -> bool {
536        self.flip_x || self.flip_y
537    }
538}
539
540// Implement Component trait so Sprite can be used in the ECS
541impl Component for Sprite {}
542
543// =============================================================================
544// Default Implementation
545// =============================================================================
546
547impl Default for Sprite {
548    /// Creates a sprite with an invalid texture handle.
549    ///
550    /// This is primarily useful for deserialization or when the texture
551    /// will be set later. The sprite will not render correctly until a
552    /// valid texture handle is assigned.
553    fn default() -> Self {
554        Self {
555            texture: AssetHandle::INVALID,
556            color: Color::WHITE,
557            source_rect: None,
558            flip_x: false,
559            flip_y: false,
560            anchor: Vec2::new(0.5, 0.5),
561            custom_size: None,
562        }
563    }
564}
565
566// =============================================================================
567// Tests
568// =============================================================================
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573
574    // Helper to create a valid handle for testing
575    fn dummy_handle() -> AssetHandle<TextureAsset> {
576        AssetHandle::new(1, 1)
577    }
578
579    #[test]
580    fn test_sprite_new() {
581        let handle = dummy_handle();
582        let sprite = Sprite::new(handle);
583
584        assert_eq!(sprite.texture, handle);
585        assert_eq!(sprite.color, Color::WHITE);
586        assert_eq!(sprite.source_rect, None);
587        assert_eq!(sprite.flip_x, false);
588        assert_eq!(sprite.flip_y, false);
589        assert_eq!(sprite.anchor, Vec2::new(0.5, 0.5));
590        assert_eq!(sprite.custom_size, None);
591    }
592
593    #[test]
594    fn test_sprite_default() {
595        let sprite = Sprite::default();
596
597        assert_eq!(sprite.texture, AssetHandle::INVALID);
598        assert_eq!(sprite.color, Color::WHITE);
599        assert_eq!(sprite.anchor, Vec2::new(0.5, 0.5));
600    }
601
602    #[test]
603    fn test_sprite_with_color() {
604        let handle = dummy_handle();
605        let red = Color::rgba(1.0, 0.0, 0.0, 0.5);
606        let sprite = Sprite::new(handle).with_color(red);
607
608        assert_eq!(sprite.color, red);
609    }
610
611    #[test]
612    fn test_sprite_with_source_rect() {
613        let handle = dummy_handle();
614        let rect = Rect::new(10.0, 20.0, 32.0, 32.0);
615        let sprite = Sprite::new(handle).with_source_rect(rect);
616
617        assert_eq!(sprite.source_rect, Some(rect));
618        assert!(sprite.has_source_rect());
619    }
620
621    #[test]
622    fn test_sprite_without_source_rect() {
623        let handle = dummy_handle();
624        let rect = Rect::new(10.0, 20.0, 32.0, 32.0);
625        let sprite = Sprite::new(handle)
626            .with_source_rect(rect)
627            .without_source_rect();
628
629        assert_eq!(sprite.source_rect, None);
630        assert!(!sprite.has_source_rect());
631    }
632
633    #[test]
634    fn test_sprite_with_flip_x() {
635        let handle = dummy_handle();
636        let sprite = Sprite::new(handle).with_flip_x(true);
637
638        assert_eq!(sprite.flip_x, true);
639        assert_eq!(sprite.flip_y, false);
640        assert!(sprite.is_flipped());
641    }
642
643    #[test]
644    fn test_sprite_with_flip_y() {
645        let handle = dummy_handle();
646        let sprite = Sprite::new(handle).with_flip_y(true);
647
648        assert_eq!(sprite.flip_x, false);
649        assert_eq!(sprite.flip_y, true);
650        assert!(sprite.is_flipped());
651    }
652
653    #[test]
654    fn test_sprite_with_flip() {
655        let handle = dummy_handle();
656        let sprite = Sprite::new(handle).with_flip(true, true);
657
658        assert_eq!(sprite.flip_x, true);
659        assert_eq!(sprite.flip_y, true);
660        assert!(sprite.is_flipped());
661    }
662
663    #[test]
664    fn test_sprite_with_anchor() {
665        let handle = dummy_handle();
666        let sprite = Sprite::new(handle).with_anchor(0.0, 1.0);
667
668        assert_eq!(sprite.anchor, Vec2::new(0.0, 1.0));
669    }
670
671    #[test]
672    fn test_sprite_with_anchor_vec() {
673        let handle = dummy_handle();
674        let anchor = Vec2::new(0.25, 0.75);
675        let sprite = Sprite::new(handle).with_anchor_vec(anchor);
676
677        assert_eq!(sprite.anchor, anchor);
678    }
679
680    #[test]
681    fn test_sprite_with_custom_size() {
682        let handle = dummy_handle();
683        let size = Vec2::new(64.0, 64.0);
684        let sprite = Sprite::new(handle).with_custom_size(size);
685
686        assert_eq!(sprite.custom_size, Some(size));
687        assert!(sprite.has_custom_size());
688    }
689
690    #[test]
691    fn test_sprite_without_custom_size() {
692        let handle = dummy_handle();
693        let size = Vec2::new(64.0, 64.0);
694        let sprite = Sprite::new(handle)
695            .with_custom_size(size)
696            .without_custom_size();
697
698        assert_eq!(sprite.custom_size, None);
699        assert!(!sprite.has_custom_size());
700    }
701
702    #[test]
703    fn test_sprite_size_or_rect_custom() {
704        let handle = dummy_handle();
705        let custom_size = Vec2::new(100.0, 100.0);
706        let sprite = Sprite::new(handle)
707            .with_custom_size(custom_size)
708            .with_source_rect(Rect::new(0.0, 0.0, 32.0, 32.0));
709
710        // Custom size takes precedence
711        assert_eq!(sprite.size_or_rect(), custom_size);
712    }
713
714    #[test]
715    fn test_sprite_size_or_rect_source() {
716        let handle = dummy_handle();
717        let sprite = Sprite::new(handle).with_source_rect(Rect::new(0.0, 0.0, 32.0, 48.0));
718
719        // Source rect size is used when no custom size
720        assert_eq!(sprite.size_or_rect(), Vec2::new(32.0, 48.0));
721    }
722
723    #[test]
724    fn test_sprite_size_or_rect_none() {
725        let handle = dummy_handle();
726        let sprite = Sprite::new(handle);
727
728        // Returns zero when neither is set (caller should query texture)
729        assert_eq!(sprite.size_or_rect(), Vec2::zero());
730    }
731
732    #[test]
733    fn test_sprite_is_flipped() {
734        let handle = dummy_handle();
735
736        let sprite1 = Sprite::new(handle);
737        assert!(!sprite1.is_flipped());
738
739        let sprite2 = Sprite::new(handle).with_flip_x(true);
740        assert!(sprite2.is_flipped());
741
742        let sprite3 = Sprite::new(handle).with_flip_y(true);
743        assert!(sprite3.is_flipped());
744
745        let sprite4 = Sprite::new(handle).with_flip(true, true);
746        assert!(sprite4.is_flipped());
747    }
748
749    #[test]
750    fn test_sprite_builder_chain() {
751        let handle = dummy_handle();
752        let sprite = Sprite::new(handle)
753            .with_color(Color::RED)
754            .with_source_rect(Rect::new(0.0, 0.0, 32.0, 32.0))
755            .with_flip(true, false)
756            .with_anchor(0.5, 1.0)
757            .with_custom_size(Vec2::new(64.0, 64.0));
758
759        assert_eq!(sprite.color, Color::RED);
760        assert_eq!(sprite.source_rect, Some(Rect::new(0.0, 0.0, 32.0, 32.0)));
761        assert_eq!(sprite.flip_x, true);
762        assert_eq!(sprite.flip_y, false);
763        assert_eq!(sprite.anchor, Vec2::new(0.5, 1.0));
764        assert_eq!(sprite.custom_size, Some(Vec2::new(64.0, 64.0)));
765    }
766
767    #[test]
768    fn test_sprite_clone() {
769        let handle = dummy_handle();
770        let sprite1 = Sprite::new(handle).with_color(Color::BLUE);
771        let sprite2 = sprite1.clone();
772
773        assert_eq!(sprite1, sprite2);
774    }
775
776    #[test]
777    fn test_sprite_is_component() {
778        // Compile-time check that Sprite implements Component
779        fn assert_component<T: Component>() {}
780        assert_component::<Sprite>();
781    }
782
783    #[test]
784    fn test_sprite_debug() {
785        let handle = dummy_handle();
786        let sprite = Sprite::new(handle);
787        let debug_str = format!("{:?}", sprite);
788
789        assert!(debug_str.contains("Sprite"));
790    }
791
792    #[test]
793    fn test_sprite_send_sync() {
794        fn assert_send_sync<T: Send + Sync>() {}
795        assert_send_sync::<Sprite>();
796    }
797}