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::rc::Rc;
10
11use sdl3::pixels::{Color, PixelFormat};
12use sdl3::Sdl;
13use sdl3::render::{Canvas, FRect};
14use sdl3::video::Window;
15
16use crate::resources::loadable::{Font, Image};
17use crate::resources::{self, LoadableResource, ResourceManager, loadable};
18
19/// Owned SDL variables used for rendering
20///
21/// - `canvas`: the drawing surface for the window
22/// - `sdl_context`: the SDL context
23pub struct Graphics {
24    /// The SDL3 canvas, required to draw
25    pub canvas: Canvas<Window>,
26    /// SDL3 contaxt
27    pub sdl_context: Sdl,
28    /// SDL3 texture creator
29    pub texture_creator: Rc<sdl3::render::TextureCreator<sdl3::video::WindowContext>>,
30    /// SDL3 TTF context required for text rendering
31    pub ttf_context: sdl3::ttf::Sdl3TtfContext,
32}
33
34impl Graphics {
35    /// Initialize SDL3, create a centered, resizable window and return a [`Graphics`]
36    /// container with the canvas and SDL context.
37    ///
38    /// # Parameters
39    /// - `name`: Window title.
40    /// - `dimensions`: (width, height) in pixels (u32).
41    ///
42    /// # Returns
43    /// - `Ok(Graphics)` on success.
44    /// - `Err(Box<dyn std::error::Error>)` on failure (window/canvas build error).
45    ///
46    /// # Example
47    /// ```ignore
48    /// let graphics = graphics::new(String::from("my cool game"), (500, 500))?;
49    /// ```
50    pub fn new(
51        name: String,
52        dimensions: (u32, u32),
53    ) -> Result<Graphics, Box<dyn std::error::Error>> {
54        // TODO: allow the user to uh customize video_subsystem configuration 'cuz man this is ass why
55        // do we position_centered() and resizable() it by default
56
57        let sdl_context = sdl3::init()?;
58        let ttf_context = sdl3::ttf::init().map_err(|e| e.to_string())?;
59        let video_subsystem = sdl_context.video()?;
60
61        let window = video_subsystem
62            .window(&name, dimensions.0, dimensions.1)
63            .position_centered()
64            .resizable()
65            .build()
66            .map_err(|e| e.to_string())?;
67
68        let canvas = window.into_canvas();
69        let texture_creator = canvas.texture_creator();
70        Ok(Graphics {
71            canvas,
72            sdl_context,
73            texture_creator: Rc::new(texture_creator),
74            ttf_context
75        })
76    }
77
78    /// Cache an image if it isn't cached and draw it on the canvas
79    ///
80    /// # Parameters
81    /// - `path`: Path to the image
82    /// - `position`: Where to draw the image in the window (x,y) in pixels (f32).
83    ///
84    /// # Returns
85    /// - `Ok(())` on success.
86    /// - `Err(Box<dyn std::error::Error>)` on failure
87    ///
88    /// # Example
89    /// ```ignore
90    /// graphics.draw_image(String::from("examples/example.png"), (0.0, 0.0)).await;
91    /// ```
92    pub fn draw_image(
93        &mut self,
94        path: String,
95        position: (f32, f32),
96        manager: &mut ResourceManager,
97    ) -> anyhow::Result<()> {
98        if !manager.is_cached(path.clone()) {
99            // rust programmers when they have to .clone()
100            let texture = loadable::Image::load(PathBuf::from(path.clone()), self, None);
101            manager.cache_asset(texture?)?; // those question marks are funny hehehe
102        }
103
104        let image: &Image = resources::as_concrete(manager.get_asset(path).unwrap())?;
105
106        let dst_rect = FRect::new(
107            position.0,
108            position.1,
109            image.width as f32,
110            image.height as f32,
111        );
112        self.canvas
113            .copy_ex(
114                &image.texture,
115                None,
116                Some(dst_rect),
117                0.0,
118                None,
119                false,
120                false,
121            )
122            .unwrap();
123
124        Ok(())
125    }
126
127    /// Create a texture from font + text and render it on the canvas
128    /// 
129    /// This does not cache the font, so you have the load all the fonts you'll be using in your
130    /// initiaization function (like `main`). This does cache the texture created from supplied
131    /// font and text.
132    pub fn draw_text(
133        &mut self,
134        text: String,
135        position: (f32, f32),
136        font_family: String,
137        color: Color,
138        manager: &mut ResourceManager
139    ) -> anyhow::Result<()> {
140        let cache_key = format!("{}|{}|{:x?}", font_family, text, color.to_u32(&PixelFormat::RGBA32));
141        let font: &Font = {
142            let asset = manager.get_asset(font_family)?;
143            resources::as_concrete(asset)?
144        };
145
146        if !manager.is_cached(cache_key.clone()) {
147            let surface = font.buffer
148                .render(&text)
149                .blended(color)
150                .map_err(|e| anyhow::anyhow!("render error: {e}"))?;
151            let image = Image::load_from_surface(cache_key.clone(), self, surface);
152            manager.cache_asset(image?)?;
153        }
154        let texture: &Image = resources::as_concrete(manager.get_asset(cache_key)?)?;
155        
156        let dst_rect = FRect::new(
157            position.0,
158            position.1,
159            texture.width as f32,
160            texture.height as f32,
161        );
162        self.canvas
163            .copy_ex(
164                &texture.texture,
165                None,
166                Some(dst_rect),
167                0.0,
168                None,
169                false,
170                false,
171            )
172            .unwrap();
173        Ok(())
174    }
175}