fennel_engine/
graphics.rs

1//! SDL3-backed graphics helper
2//!
3//! Provides:
4//! - `Graphics`: owned SDL context + drawing canvas
5//! - `Graphics::new(...)`: initialize SDL, create a centered resizable window and return [`Graphics`]
6//!
7
8use std::path::PathBuf;
9use std::sync::Arc;
10
11use sdl3::Sdl;
12use sdl3::render::{Canvas, FRect};
13use sdl3::video::Window;
14
15use crate::resources::loadable::Image;
16use crate::resources::{self, LoadableResource, ResourceManager, loadable};
17
18/// Owned SDL variables used for rendering
19///
20/// - `canvas`: the drawing surface for the window
21/// - `sdl_context`: the SDL context
22pub struct Graphics {
23    /// The SDL3 canvas, required to draw
24    pub canvas: Canvas<Window>,
25    /// SDL3 contaxt
26    pub sdl_context: Sdl,
27    /// SDL3 texture creator
28    pub texture_creator: Arc<sdl3::render::TextureCreator<sdl3::video::WindowContext>>,
29    /// SDL3 TTF context required for text rendering
30    pub ttf_context: sdl3::ttf::Sdl3TtfContext,
31}
32
33impl Graphics {
34    /// Initialize SDL3, create a centered, resizable window and return a [`Graphics`]
35    /// container with the canvas and SDL context.
36    ///
37    /// # Parameters
38    /// - `name`: Window title.
39    /// - `dimensions`: (width, height) in pixels (u32).
40    ///
41    /// # Returns
42    /// - `Ok(Graphics)` on success.
43    /// - `Err(Box<dyn std::error::Error>)` on failure (window/canvas build error).
44    ///
45    /// # Example
46    /// ```ignore
47    /// let graphics = graphics::new(String::from("my cool game"), (500, 500))?;
48    /// ```
49    pub fn new(
50        name: String,
51        dimensions: (u32, u32),
52    ) -> Result<Graphics, Box<dyn std::error::Error>> {
53        // TODO: allow the user to uh customize video_subsystem configuration 'cuz man this is ass why
54        // do we position_centered() and resizable() it by default
55
56        let sdl_context = sdl3::init()?;
57        let ttf_context = sdl3::ttf::init().map_err(|e| e.to_string())?;
58        let video_subsystem = sdl_context.video()?;
59
60        let window = video_subsystem
61            .window(&name, dimensions.0, dimensions.1)
62            .position_centered()
63            .resizable()
64            .build()
65            .map_err(|e| e.to_string())?;
66
67        let canvas = window.into_canvas();
68        let texture_creator = canvas.texture_creator();
69        Ok(Graphics {
70            canvas,
71            sdl_context,
72            texture_creator: Arc::new(texture_creator),
73            ttf_context
74        })
75    }
76
77    /// Cache an image if it isn't cached and draw it on the canvas
78    ///
79    /// # Parameters
80    /// - `path`: Path to the image
81    /// - `position`: Where to draw the image in the window (x,y) in pixels (f32).
82    ///
83    /// # Returns
84    /// - `Ok(())` on success.
85    /// - `Err(Box<dyn std::error::Error>)` on failure
86    ///
87    /// # Example
88    /// ```ignore
89    /// graphics.draw_image(String::from("examples/example.png"), (0.0, 0.0)).await;
90    /// ```
91    pub fn draw_image(
92        &mut self,
93        path: String,
94        position: (f32, f32),
95        manager: &mut ResourceManager,
96    ) -> anyhow::Result<()> {
97        if !manager.is_cached(path.clone()) {
98            // rust programmers when they have to .clone()
99            let texture = loadable::Image::load(PathBuf::from(path.clone()), self, None);
100            manager.cache_asset(texture?)?; // those question marks are funny hehehe
101        }
102
103        let image: &Image = resources::as_concrete(manager.get_asset(path).unwrap())?;
104
105        let dst_rect = FRect::new(
106            position.0,
107            position.1,
108            image.width as f32,
109            image.height as f32,
110        );
111        self.canvas
112            .copy_ex(
113                &image.texture,
114                None,
115                Some(dst_rect),
116                0.0,
117                None,
118                false,
119                false,
120            )
121            .unwrap();
122
123        Ok(())
124    }
125}