pix_engine/
texture.rs

1//! `Texture` methods.
2//!
3//! Provides texture creation and rendering methods on [`PixState`].
4//!
5//! Provided methods:
6//!
7//! - [`PixState::texture`]: Render a portion of a texture to the current canvas.
8//! - [`PixState::texture_transformed`]: Render a transformed portion of a texture to the current
9//!   canvas.
10//! - [`PixState::create_texture`]: Creates a new texture to render to.
11//! - [`PixState::delete_texture`]: Delete a texture.
12//! - [`PixState::update_texture`]: Update texture with [u8] [slice] of pixel data.
13//! - [`PixState::set_texture_target`]: Target a texture for rendering.
14//! - [`PixState::clear_texture_target`]: Clear texture target back to primary canvas for rendering.
15//!
16//! # Example
17//!
18//! ```
19//! # use pix_engine::prelude::*;
20//! # struct App;
21//! # impl PixEngine for App {
22//! fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
23//!     let texture_id1 = s.create_texture(500, 600, PixelFormat::Rgb)?;
24//!     // Does not actually render to the current canvas
25//!     s.set_texture_target(texture_id1)?;
26//!     s.background(Color::random());
27//!     s.text("Rendered texture!")?;
28//!     s.clear_texture_target();
29//!
30//!     // `None` uses PixelFormat::default() which defaults to PixelFormat::Rgba
31//!     let texture_id2 = s.create_texture(500, 600, None)?;
32//!
33//!     // `None` updates the entire texture, pass a Rect<i32> to update a sub-rectangle area
34//!     let image = Image::from_file("./some_image.png")?;
35//!     let pitch = image.width() as usize;
36//!     s.update_texture(texture_id2, None, image.as_bytes(), pitch)?;
37//!
38//!     // Draw both textures to the current canvas
39//!     s.texture(texture_id1, None, rect![0, 0, 500, 600])?;
40//!     s.texture(texture_id2, None, rect![500, 0, 500, 600])?;
41//!
42//!     // These could be stored in `self` to avoid re-creating every frame
43//!     s.delete_texture(texture_id1)?;
44//!     s.delete_texture(texture_id2)?;
45//!     Ok(())
46//! }
47//! # }
48//! ```
49
50use crate::prelude::*;
51use std::{
52    fmt,
53    ops::{Deref, DerefMut},
54};
55
56/// `Texture` identifier used to reference and target an internally managed texture.
57#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
58pub struct TextureId(pub(crate) usize);
59
60impl fmt::Display for TextureId {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        write!(f, "{}", self.0)
63    }
64}
65
66impl Deref for TextureId {
67    type Target = usize;
68    fn deref(&self) -> &Self::Target {
69        &self.0
70    }
71}
72
73impl DerefMut for TextureId {
74    fn deref_mut(&mut self) -> &mut Self::Target {
75        &mut self.0
76    }
77}
78
79impl PixState {
80    /// Draw a portion `src` of a texture to the current render target translated and resized to
81    /// the target `dst`. Passing `None` for `src` renders the entire texture. Passing `None` for
82    /// `dst` renders to the maximum size of the render target.
83    ///
84    /// # Note
85    ///
86    /// It's possible to render one texture onto another texture, but currently they both have to
87    /// have been created in the same window. Attempting to render to a texture created with
88    /// another window will result in a [`Error::InvalidTexture`]. This restriction may be
89    /// lifted in the future.
90    ///
91    /// # Errors
92    ///
93    /// Returns an error for any of the following:
94    ///     - The current render target is closed or dropped.
95    ///     - The texture being rendered has been dropped.
96    ///     - The target texture is the same as the texture being rendered.
97    ///     - The renderer fails to draw to the texture.
98    ///
99    /// # Example
100    ///
101    /// ```
102    /// # use pix_engine::prelude::*;
103    /// # struct App { texture_id: TextureId };
104    /// # impl PixEngine for App {
105    /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
106    ///     s.set_texture_target(self.texture_id)?;
107    ///     s.background(Color::random());
108    ///     s.text("Rendered texture!")?;
109    ///     s.clear_texture_target();
110    ///
111    ///     let src = rect![10, 10, 100, 100]; // Render a sub-section of the texture
112    ///     // translate and scale texture
113    ///     let dst = rect![200, 200, 200, 200];
114    ///     s.texture(self.texture_id, src, dst)?;
115    ///     Ok(())
116    /// }
117    /// # }
118    /// ```
119    pub fn texture<R1, R2>(&mut self, texture_id: TextureId, src: R1, dst: R2) -> PixResult<()>
120    where
121        R1: Into<Option<Rect<i32>>>,
122        R2: Into<Option<Rect<i32>>>,
123    {
124        self.renderer
125            .texture(texture_id, src.into(), dst.into(), 0.0, None, None, None)
126    }
127
128    /// Draw a transformed portion `src` of a texture to the current render target translated and
129    /// resized to the target `dst`, optionally rotated by an `angle` about a `center` point or
130    /// `flipped`. `angle` can be in either radians or degrees based on [`AngleMode`]. Passing
131    /// `None` for `src` renders the entire texture. Passing `None` for `dst` renders to the
132    /// maximum size of the render target. [`PixState::image_tint`] can optionally add a tint color
133    /// to the rendered texture.
134    ///
135    /// # Note
136    ///
137    /// It's possible to render one texture onto another texture, but currently they both have to
138    /// have been created in the same window. Attempting to render to a texture created with
139    /// another window will result in a [`Error::InvalidTexture`]. This restriction may be
140    /// lifted in the future.
141    ///
142    /// # Errors
143    ///
144    /// Returns an error for any of the following:
145    ///     - The current render target is closed or dropped.
146    ///     - The texture being rendered has been dropped.
147    ///     - The target texture is the same as the texture being rendered.
148    ///     - The renderer fails to draw to the texture.
149    ///
150    /// # Example
151    ///
152    /// ```
153    /// # use pix_engine::prelude::*;
154    /// # struct App { texture_id: TextureId };
155    /// # impl PixEngine for App {
156    /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
157    ///     s.set_texture_target(self.texture_id)?;
158    ///     s.background(Color::random());
159    ///     s.text("Rendered texture!")?;
160    ///     s.clear_texture_target();
161    ///
162    ///     let src = None;
163    ///     // translate and scale texture
164    ///     let dst = rect![200, 200, 200, 200];
165    ///     let angle = 10.0;
166    ///     let center = point!(10, 10);
167    ///     s.texture_transformed(
168    ///         self.texture_id,
169    ///         src,
170    ///         dst,
171    ///         angle,
172    ///         center,
173    ///         Flipped::Horizontal,
174    ///     )?;
175    ///     Ok(())
176    /// }
177    /// # }
178    /// ```
179    pub fn texture_transformed<R1, R2, C, F>(
180        &mut self,
181        texture_id: TextureId,
182        src: R1,
183        dst: R2,
184        mut angle: f64,
185        center: C,
186        flipped: F,
187    ) -> PixResult<()>
188    where
189        R1: Into<Option<Rect<i32>>>,
190        R2: Into<Option<Rect<i32>>>,
191        C: Into<Option<Point<i32>>>,
192        F: Into<Option<Flipped>>,
193    {
194        let s = &self.settings;
195        if s.angle_mode == AngleMode::Radians {
196            angle = angle.to_degrees();
197        };
198        self.renderer.texture(
199            texture_id,
200            src.into(),
201            dst.into(),
202            angle,
203            center.into(),
204            flipped.into(),
205            s.image_tint,
206        )
207    }
208
209    /// Constructs a `Texture` to render to. Passing `None` for [`PixelFormat`] will use
210    /// [`PixelFormat::default`]. The texture will be created and tied to the current window
211    /// target. To create a texture for a window other than the primary window, call
212    /// [`PixState::set_window`].
213    ///
214    /// # Errors
215    ///
216    /// If the current window target is closed or invalid, or the texture dimensions are invalid,
217    /// then an error is returned.
218    ///
219    /// # Note
220    ///
221    /// Textures are automatically dropped when the window they were created in is closed due to an
222    /// implicit lifetime that the texture can not outlive the window it was created for. Calling
223    /// this method will create a texture for the current `window_target`, which can only be
224    /// changed using the [`PixState::set_window_target`] method. It is the responsibility of the
225    /// caller to manage created textures and call [`PixState::delete_texture`] when a texture
226    /// resource is no longer needed and to ensure that texture methods are not called for a given
227    /// window after it has been closed, otherwise an error will be returned.
228    ///
229    /// This constraint arises due to lifetime issues with SDL textures, See
230    /// <https://github.com/Rust-SDL2/rust-sdl2/issues/1107> for more details.
231    ///
232    /// # Example
233    ///
234    /// ```
235    /// # use pix_engine::prelude::*;
236    /// # struct App { texture_id: TextureId };
237    /// # impl PixEngine for App {
238    /// # fn on_update(&mut self, s: &mut PixState) -> PixResult<()> { Ok(()) }
239    /// fn on_start(&mut self, s: &mut PixState) -> PixResult<()> {
240    ///     self.texture_id = s.create_texture(
241    ///         s.width()? / 2,
242    ///         s.height()? / 2,
243    ///         PixelFormat::Rgb,
244    ///     )?;
245    ///     Ok(())
246    /// }
247    /// # }
248    /// ```
249    pub fn create_texture<F>(&mut self, width: u32, height: u32, format: F) -> PixResult<TextureId>
250    where
251        F: Into<Option<PixelFormat>>,
252    {
253        self.renderer.create_texture(width, height, format.into())
254    }
255
256    /// Delete a `Texture`.
257    ///
258    /// # Errors
259    ///
260    /// If the current window target is closed or invalid, or the texture has already been dropped,
261    /// then an error is returned.
262    ///
263    /// # Note
264    ///
265    /// Currently, it is up to the caller to manage valid textures. Textures become invalid
266    /// whenever the `window_target` they were created in has been closed. Calling any texture
267    /// methods with an invalid `TextureId` will result in an error.
268    ///
269    /// This constraint arises due to lifetime issues with SDL textures, See
270    /// <https://github.com/Rust-SDL2/rust-sdl2/issues/1107> for more details.
271    ///
272    /// # Example
273    ///
274    /// ```
275    /// # use pix_engine::prelude::*;
276    /// # struct App;
277    /// # impl PixEngine for App {
278    /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
279    ///     // A more polished implementation would manage state in `self` to avoid re-creating and
280    ///     // destroying textures every frame
281    ///     let texture_id = s.create_texture(500, 600, PixelFormat::Rgb)?;
282    ///     // Render things
283    ///     s.delete_texture(texture_id)?;
284    ///     Ok(())
285    /// }
286    /// # }
287    /// ```
288    pub fn delete_texture(&mut self, texture_id: TextureId) -> PixResult<()> {
289        self.renderer.delete_texture(texture_id)
290    }
291
292    /// Update the `Texture` with a [u8] [slice] of pixel data. Passing `None` for `rect` updates
293    /// the entire texture. `pitch` is the number of bytes in a row of pixels data including
294    /// padding between lines.
295    /// # Errors
296    ///
297    /// If the window in which the texture was created is closed, or the renderer fails
298    /// to update to the texture, then an error is returned.
299    ///
300    /// # Example
301    ///
302    /// ```
303    /// # use pix_engine::prelude::*;
304    /// # struct App { texture_id: TextureId };
305    /// # impl PixEngine for App {
306    /// # fn on_update(&mut self, s: &mut PixState) -> PixResult<()> { Ok(()) }
307    /// fn on_start(&mut self, s: &mut PixState) -> PixResult<()> {
308    ///     self.texture_id = s.create_texture(500, 600, None)?;
309    ///     let image = Image::from_file("./some_image.png")?;
310    ///     s.update_texture(self.texture_id, None, image.as_bytes(), image.pitch())?;
311    ///     Ok(())
312    /// }
313    /// # }
314    /// ```
315    pub fn update_texture<R, P>(
316        &mut self,
317        texture_id: TextureId,
318        rect: R,
319        pixels: P,
320        pitch: usize,
321    ) -> PixResult<()>
322    where
323        R: Into<Option<Rect<i32>>>,
324        P: AsRef<[u8]>,
325    {
326        let rect = rect.into();
327        let pixels = pixels.as_ref();
328        self.renderer
329            .update_texture(texture_id, rect, pixels, pitch)
330    }
331
332    /// Set a `Texture` as the priamry target for drawing operations. Pushes current settings and UI
333    /// cursor to the stack, so any changes made while a texture target is set will be in effect
334    /// until [`PixState::reset_texture_target`] is called.
335    ///
336    /// # Errors
337    ///
338    /// If the target has been dropped or is invalid, then an error is returned.
339    ///
340    /// # Example
341    ///
342    /// ```
343    /// # use pix_engine::prelude::*;
344    /// # struct App { texture_id: TextureId };
345    /// # impl PixEngine for App {
346    /// # fn on_update(&mut self, s: &mut PixState) -> PixResult<()> { Ok(()) }
347    /// fn on_start(&mut self, s: &mut PixState) -> PixResult<()> {
348    ///     self.texture_id = s.create_texture(500, 600, None)?;
349    ///     s.set_texture_target(self.texture_id)?;
350    ///     s.background(Color::random());
351    ///     s.text("Rendered texture!")?;
352    ///     s.clear_texture_target();
353    ///     Ok(())
354    /// }
355    /// # }
356    /// ```
357    pub fn set_texture_target(&mut self, id: TextureId) -> PixResult<()> {
358        if self.renderer.texture_target().is_none() {
359            self.push();
360            self.ui.push_cursor();
361            self.set_cursor_pos(self.theme.spacing.frame_pad);
362            self.renderer.set_texture_target(id)
363        } else {
364            Ok(())
365        }
366    }
367
368    /// Clears `Texture` target back to the primary canvas for drawing operations. Pops previous
369    /// settings and UI cursor off the stack, so that changes made while texture target was set are
370    /// reverted.
371    pub fn clear_texture_target(&mut self) {
372        if self.renderer.texture_target().is_some() {
373            self.renderer.clear_texture_target();
374            self.ui.pop_cursor();
375            self.pop();
376        }
377    }
378}
379
380/// Trait for texture operations on the underlying `Renderer`.
381pub(crate) trait TextureRenderer {
382    /// Create a `Texture` to draw to.
383    ///
384    /// # Errors
385    ///
386    /// If the current window target is closed or invalid, or the texture dimensions are invalid,
387    /// then an error is returned.
388    fn create_texture(
389        &mut self,
390        width: u32,
391        height: u32,
392        format: Option<PixelFormat>,
393    ) -> PixResult<TextureId>;
394
395    /// Delete a `Texture`.
396    ///
397    /// # Errors
398    ///
399    /// If the current window target is closed or invalid, or the texture has already been dropped,
400    /// then an error is returned.
401    ///
402    fn delete_texture(&mut self, texture_id: TextureId) -> PixResult<()>;
403
404    /// Update texture with pixel data.
405    ///
406    /// # Errors
407    ///
408    /// If the current window target is closed or invalid, or the renderer fails to update to the
409    /// texture, then an error is returned.
410    fn update_texture<P: AsRef<[u8]>>(
411        &mut self,
412        texture_id: TextureId,
413        rect: Option<Rect<i32>>,
414        pixels: P,
415        pitch: usize,
416    ) -> PixResult<()>;
417
418    /// Draw texture to the curent canvas.
419    ///
420    /// # Errors
421    ///
422    /// Returns an error for any of the following:
423    ///     - The current render target is closed or dropped.
424    ///     - The texture being rendered has been dropped.
425    ///     - The target texture is the same as the texture being rendered.
426    ///     - The renderer fails to draw to the texture.
427    ///
428    #[allow(clippy::too_many_arguments)]
429    fn texture(
430        &mut self,
431        texture_id: TextureId,
432        src: Option<Rect<i32>>,
433        dst: Option<Rect<i32>>,
434        angle: f64,
435        center: Option<Point<i32>>,
436        flipped: Option<Flipped>,
437        tint: Option<Color>,
438    ) -> PixResult<()>;
439
440    /// Returns texture used as the target for drawing operations, if set.
441    fn texture_target(&self) -> Option<TextureId>;
442
443    /// Set a `Texture` as the primary target for drawing operations instead of the window target
444    /// canvas.
445    ///
446    /// # Errors
447    ///
448    /// If the texture has been dropped or is invalid, then an error is returned.
449    fn set_texture_target(&mut self, texture_id: TextureId) -> PixResult<()>;
450
451    /// Clear `Texture` target back to the window target canvas for drawing operations.
452    fn clear_texture_target(&mut self);
453
454    /// Returns whether a texture is set as the target for drawing operations.
455    fn has_texture_target(&self) -> bool;
456
457    /// Clear internal texture cache.
458    fn clear_texture_cache(&mut self);
459}