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}