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