fennel_core/
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::font::{DummyFont, Font};
20use crate::resources::image::Image;
21use crate::resources::{self, LoadableResource, ResourceManager};
22
23pub trait HasWindow {
24    fn window_mut(&mut self) -> &mut crate::Window;
25}
26
27/// Owned SDL variables used for rendering
28///
29/// - `canvas`: the drawing surface for the window
30/// - `sdl_context`: the SDL context
31pub struct Graphics {
32    /// The SDL3 canvas, required to draw
33    pub canvas: Canvas<Window>,
34    /// SDL3 contaxt
35    pub sdl_context: Sdl,
36    /// SDL3 texture creator
37    pub texture_creator: Rc<sdl3::render::TextureCreator<sdl3::video::WindowContext>>,
38    /// SDL3 TTF context required for text rendering
39    pub ttf_context: sdl3::ttf::Sdl3TtfContext,
40    /// Reference to [`resources::ResourceManager`]
41    resource_manager: Arc<Mutex<ResourceManager>>,
42}
43
44/// Config for [`Graphics::new`] and for [`GraphicsBuilder`]
45#[derive(Default, Debug)]
46pub struct WindowConfig {
47    /// Is the window resizable?
48    pub resizable: bool,
49    /// Is the window fullscreen?
50    pub fullscreen: bool,
51    /// Is the window centered by default?
52    pub centered: bool
53}
54
55/// Builder for creating a Graphics instance.
56pub struct GraphicsBuilder<F>
57where 
58    F: Fn(&mut Graphics)
59{
60    resource_manager: Option<Arc<Mutex<ResourceManager>>>,
61    dimensions: (u32, u32),
62    name: String,
63    initializer: Option<F>,
64    config: WindowConfig
65}
66
67impl<F> GraphicsBuilder<F> 
68where 
69    F: Fn(&mut Graphics)
70{
71    /// Create a new empty GraphicsBuilder
72    /// By default there is no resource manager or resource initializer, dimensions are 0, 0, name
73    /// is empty
74    pub fn new() -> GraphicsBuilder<F> {
75        GraphicsBuilder { 
76            resource_manager: None,
77            dimensions: (0, 0),
78            name: "".to_string(),
79            initializer: None,
80            config: WindowConfig {
81                resizable: false,
82                fullscreen: false,
83                centered: false
84            }
85        }
86    }
87
88    /// Set the resource manager
89    pub fn resource_manager(mut self, resource_manager: Arc<Mutex<ResourceManager>>) -> GraphicsBuilder<F> {
90        self.resource_manager = Some(resource_manager);
91        self
92    }
93
94    /// Set the window dimensions
95    pub fn dimensions(mut self, dimensions: (u32, u32)) -> GraphicsBuilder<F> {
96        self.dimensions = dimensions;
97        self
98    }
99
100    /// Set the window name
101    pub fn window_name(mut self, name: String) -> GraphicsBuilder<F> {
102        self.name = name;
103        self
104    }
105
106    /// Set the resource initializer (closure)
107    pub fn initializer(mut self, initializer: F) -> GraphicsBuilder<F> where 
108        F: Fn(&mut Graphics)
109    {
110        self.initializer = Some(initializer);
111        self
112    }
113
114    /// Will the window be resizable?
115    pub fn resizable(mut self, resizable: bool) -> GraphicsBuilder<F> {
116        self.config.resizable = resizable;
117        self
118    }
119
120    /// Will the window be fullscreen?
121    pub fn fullscreen(mut self, fullscreen: bool) -> GraphicsBuilder<F> {
122        self.config.fullscreen = fullscreen;
123        self
124    }
125    
126    /// Will the window be centered?
127    pub fn centered(mut self, centered: bool) -> GraphicsBuilder<F> {
128        self.config.centered = centered;
129        self
130    }
131
132    /// Build `Graphics`
133    ///
134    /// # Panic
135    /// Panics if no resource manager or initializer was provided
136    pub fn build(self) -> anyhow::Result<Graphics> {
137        Ok(Graphics::new(
138            self.name, 
139            self.dimensions, 
140            self.resource_manager.expect("no resource manager provided"),
141            self.initializer.expect("no resource initializer provided"),
142            self.config
143        ).unwrap())
144    }
145}
146
147impl<F> Default for GraphicsBuilder<F>
148where 
149    F: Fn(&mut Graphics)
150{
151    /// Default implementation delegates to `[GraphicsBuilder::new]`
152    fn default() -> Self {
153        Self::new()
154    }
155}
156
157impl Graphics {
158    /// Initialize SDL3, create a centered, resizable window and return a [`Graphics`]
159    /// container with the canvas and SDL context.
160    ///
161    /// # Parameters
162    /// - `name`: Window title.
163    /// - `dimensions`: (width, height) in pixels (u32).
164    ///
165    /// # Returns
166    /// - `Ok(Graphics)` on success.
167    /// - `Err(Box<dyn std::error::Error>)` on failure (window/canvas build error).
168    ///
169    /// # Example
170    /// ```ignore
171    /// let graphics = graphics::new(String::from("my cool game"), (500, 500), resource_manager, |_| {}, config)?;
172    /// ```
173    pub fn new<F>(
174        name: String,
175        dimensions: (u32, u32),
176        resource_manager: Arc<Mutex<ResourceManager>>,
177        resource_initialization: F,
178        config: WindowConfig
179    ) -> Result<Graphics, Box<dyn std::error::Error>> where 
180        F: Fn(&mut Graphics) {
181        // TODO: allow the user to uh customize video_subsystem configuration 'cuz man this is ass why
182        // do we position_centered() and resizable() it by default
183
184        let sdl_context = sdl3::init()?;
185        let ttf_context = sdl3::ttf::init().map_err(|e| e.to_string())?;
186        let video_subsystem = sdl_context.video()?;
187
188        let mut builder = video_subsystem
189            .window(&name, dimensions.0, dimensions.1);
190
191        let _ = if config.centered { builder.position_centered() } else { &mut builder };
192        let _ = if config.resizable { builder.resizable() } else { &mut builder };
193        let _ = if config.fullscreen { builder.fullscreen() } else { &mut builder };
194
195        let window = builder
196            .build()
197            .map_err(|e| e.to_string())?;
198
199        let canvas = window.into_canvas();
200        let texture_creator = canvas.texture_creator();
201        let mut graphics = Graphics {
202            canvas,
203            sdl_context,
204            texture_creator: Rc::new(texture_creator),
205            ttf_context,
206            resource_manager,
207        };
208
209        resource_initialization(&mut graphics);
210
211        Ok(graphics)
212    }
213
214    /// Cache an image if it isn't cached and draw it on the canvas
215    ///
216    /// # Parameters
217    /// - `path`: Path to the image
218    /// - `position`: Where to draw the image in the window (x,y) in pixels (f32).
219    ///
220    /// # Returns
221    /// - `Ok(())` on success.
222    /// - `Err(Box<dyn std::error::Error>)` on failure
223    ///
224    /// # Example
225    /// ```ignore
226    /// graphics.draw_image(String::from("examples/example.png"), (0.0, 0.0)).await;
227    /// ```
228    pub fn draw_image(&mut self, path: String, position: (f32, f32)) -> anyhow::Result<()> {
229        let manager = self.resource_manager.clone();
230        let mut manager = manager
231            .try_lock()
232            .context("failed to lock resource_manager")
233            .unwrap();
234
235        if !manager.is_cached(path.clone()) {
236            // rust programmers when they have to .clone()
237            let texture = Image::load(PathBuf::from(path.clone()), "".to_string(), self, None);
238            manager.cache_asset(texture?)?; // those question marks are funny hehehe
239        }
240
241        let image: &Image = resources::downcast_ref(manager.get_asset(path).unwrap())?;
242
243        let dst_rect = FRect::new(
244            position.0,
245            position.1,
246            image.width as f32,
247            image.height as f32,
248        );
249        self.canvas
250            .copy_ex(
251                &image.texture,
252                None,
253                Some(dst_rect),
254                0.0,
255                None,
256                false,
257                false,
258            )
259            .unwrap();
260
261        Ok(())
262    }
263
264    /// Create a texture from font + text and render it on the canvas
265    pub fn draw_text(
266        &mut self,
267        text: String,
268        position: (f32, f32),
269        font: String,
270        color: Color,
271        size: f32,
272    ) -> anyhow::Result<()> {
273        let manager = self.resource_manager.clone();
274        let mut manager = manager
275            .try_lock()
276            .context("failed to lock resource_manager")
277            .unwrap();
278
279        // dumbass solution. either way, i see no other solution to this.
280        // as sdl3 requires us to create a texture from font to draw text,
281        // we will be caching it as an [`resources::loadable::Image`] under this key
282        let cache_key = format!(
283            "{}|{}|{}|{:x?}",
284            font,
285            text,
286            size,
287            color.to_u32(&PixelFormat::RGBA32)
288        );
289        let font: &DummyFont = {
290            let asset = manager.get_asset(font)?;
291            resources::downcast_ref(asset)?
292        };
293
294        let font_key = format!("{}|{}", font.name(), size);
295
296        if !manager.is_cached(font_key.clone()) {
297            let asset = Font::load(font.path.clone(), font_key.clone(), self, Some(size));
298            manager.cache_asset(asset?)?;
299        }
300
301        if !manager.is_cached(cache_key.clone()) {
302            let font = resources::downcast_ref::<Font>(manager.get_asset(font_key)?)?;
303            let surface = font
304                .buffer
305                .render(&text)
306                .blended(color)
307                .map_err(|e| anyhow::anyhow!("render error: {e}"))?;
308            let image = Image::load_from_surface(cache_key.clone(), self, surface);
309            manager.cache_asset(image?)?;
310        }
311        let texture: &Image = resources::downcast_ref(manager.get_asset(cache_key)?)?;
312
313        let dst_rect = FRect::new(
314            position.0,
315            position.1,
316            texture.width as f32,
317            texture.height as f32,
318        );
319        self.canvas
320            .copy_ex(
321                &texture.texture,
322                None,
323                Some(dst_rect),
324                0.0,
325                None,
326                false,
327                false,
328            )
329            .unwrap();
330        Ok(())
331    }
332}