fennel_engine/
graphics.rs

1//! SDL3-backed graphics helper
2//!
3//! Provides:
4//! - `Graphics`: owned SDL context + drawing canvas
5//! - `init(...)`: initialize SDL, create a centered resizable window and return `Graphics`
6//!
7
8use std::collections::HashMap;
9use std::path::Path;
10
11use image::ImageReader;
12use sdl3::Sdl;
13use sdl3::pixels::PixelFormat;
14use sdl3::render::{Canvas, FRect};
15use sdl3::video::Window;
16
17/// Owned SDL variables used for rendering
18///
19/// - `canvas`: the drawing surface for the window
20/// - `sdl_context`: the SDL context
21pub struct Graphics {
22    pub canvas: Canvas<Window>,
23    pub sdl_context: Sdl,
24    pub texture_creator: sdl3::render::TextureCreator<sdl3::video::WindowContext>,
25    texture_cache: HashMap<String, CachedTexture>,
26}
27
28#[derive(Clone)]
29struct CachedTexture {
30    buffer: Vec<u8>,
31    width: u32,
32    height: u32,
33}
34
35impl Graphics {
36    /// Initialize SDL3, create a centered, resizable window and return a [`Graphics`]
37    /// container with the canvas and SDL context.
38    ///
39    /// # Parameters
40    /// - `name`: Window title.
41    /// - `dimensions`: (width, height) in pixels (u32).
42    ///
43    /// # Returns
44    /// - `Ok(Graphics)` on success.
45    /// - `Err(Box<dyn std::error::Error>)` on failure (window/canvas build error).
46    ///
47    /// # Example
48    /// ```no_run
49    /// let graphics = graphics::init(String::from("my cool game"), (500, 500));
50    /// ```
51    pub fn new(
52        name: String,
53        dimensions: (u32, u32),
54    ) -> Result<Graphics, Box<dyn std::error::Error>> {
55        // TODO: allow the user to uh customize video_subsystem configuration 'cuz man this is ass why
56        // do we position_centered() and resizable() it by default
57
58        let sdl_context = sdl3::init().unwrap();
59        let video_subsystem = sdl_context.video().unwrap(); // TODO: get this fucking unwrap out of
60        // here and replace with something more
61        // cool
62
63        let window = video_subsystem
64            .window(&name, dimensions.0, dimensions.1)
65            .position_centered()
66            .resizable()
67            .build()
68            .map_err(|e| e.to_string())?;
69
70        let canvas = window.into_canvas();
71        let texture_creator = canvas.texture_creator();
72        Ok(Graphics {
73            canvas,
74            sdl_context,
75            texture_creator,
76            texture_cache: HashMap::new(),
77        })
78    }
79
80    pub fn draw_image(&mut self, path: String, position: (f32, f32)) -> anyhow::Result<()> {
81        if !self.texture_cache.contains_key(&path) {
82            let path = Path::new(&path);
83            let img = ImageReader::open(path)?.decode()?;
84            let buffer = img.to_rgba8().into_raw();
85
86            self.texture_cache.insert(
87                path.display().to_string(),
88                CachedTexture {
89                    buffer,
90                    width: img.width(),
91                    height: img.height(),
92                },
93            );
94        }
95        let buffer: &mut CachedTexture = &mut self.texture_cache.get(&path).unwrap().clone(); // .unwrap() should be safe here
96        // because in the previous if block we
97        // make sure there is a texture with
98        // this key in cache
99        // otherwise i dunno :3
100        let surface = sdl3::surface::Surface::from_data(
101            buffer.buffer.as_mut_slice(),
102            buffer.width,
103            buffer.height,
104            buffer.width * 4,
105            PixelFormat::RGBA32,
106        )
107        .map_err(|e| e.to_string())
108        .unwrap();
109
110        let dst_rect = FRect::new(
111            position.0,
112            position.1,
113            surface.width() as f32,
114            surface.height() as f32,
115        );
116
117        let texture = self
118            .texture_creator
119            .create_texture_from_surface(surface)
120            .map_err(|e| e.to_string())
121            .unwrap();
122
123        self.canvas
124            .copy_ex(&texture, None, Some(dst_rect), 0.0, None, false, false)
125            .unwrap();
126
127        Ok(())
128    }
129}