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;
9
10use sdl3::Sdl;
11use sdl3::pixels::PixelFormat;
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: sdl3::render::TextureCreator<sdl3::video::WindowContext>,
29}
30
31impl Graphics {
32 /// Initialize SDL3, create a centered, resizable window and return a [`Graphics`]
33 /// container with the canvas and SDL context.
34 ///
35 /// # Parameters
36 /// - `name`: Window title.
37 /// - `dimensions`: (width, height) in pixels (u32).
38 ///
39 /// # Returns
40 /// - `Ok(Graphics)` on success.
41 /// - `Err(Box<dyn std::error::Error>)` on failure (window/canvas build error).
42 ///
43 /// # Example
44 /// ```ignore
45 /// let graphics = graphics::new(String::from("my cool game"), (500, 500))?;
46 /// ```
47 pub fn new(
48 name: String,
49 dimensions: (u32, u32),
50 ) -> Result<Graphics, Box<dyn std::error::Error>> {
51 // TODO: allow the user to uh customize video_subsystem configuration 'cuz man this is ass why
52 // do we position_centered() and resizable() it by default
53
54 let sdl_context = sdl3::init().unwrap();
55 let video_subsystem = sdl_context.video().unwrap(); // TODO: get this fucking unwrap out of
56 // here and replace with something more
57 // cool
58
59 let window = video_subsystem
60 .window(&name, dimensions.0, dimensions.1)
61 .position_centered()
62 .resizable()
63 .build()
64 .map_err(|e| e.to_string())?;
65
66 let canvas = window.into_canvas();
67 let texture_creator = canvas.texture_creator();
68 Ok(Graphics {
69 canvas,
70 sdl_context,
71 texture_creator,
72 })
73 }
74
75 /// Cache an image if it isn't cached and draw it on the canvas
76 ///
77 /// # Parameters
78 /// - `path`: Path to the image
79 /// - `position`: Where to draw the image in the window (x,y) in pixels (f32).
80 ///
81 /// # Returns
82 /// - `Ok(())` on success.
83 /// - `Err(Box<dyn std::error::Error>)` on failure
84 ///
85 /// # Example
86 /// ```ignore
87 /// graphics.draw_image(String::from("examples/example.png"), (0.0, 0.0));
88 /// ```
89 pub fn draw_image(
90 &mut self,
91 path: String,
92 position: (f32, f32),
93 manager: &mut ResourceManager,
94 ) -> anyhow::Result<()> {
95 if !manager.is_cached(path.clone()) {
96 // rust programmers when they have to .clone()
97 let texture = loadable::Image::load(PathBuf::from(path.clone()));
98 manager.cache_asset(texture?)?; // those question marks are funny hehehe
99 }
100
101 let image: &Image = resources::as_concrete(manager.get_asset(path).unwrap());
102 // .unwrap() in the line above should be safe
103 // because in the previous if block we
104 // make sure there is a texture with
105 // this key in cache
106 // otherwise i dunno :3
107 let surface = sdl3::surface::Surface::from_data(
108 image.as_mut_slice(),
109 image.width,
110 image.height,
111 image.width * 4,
112 PixelFormat::RGBA32,
113 )
114 .map_err(|e| e.to_string())
115 .unwrap();
116
117 let dst_rect = FRect::new(
118 position.0,
119 position.1,
120 surface.width() as f32,
121 surface.height() as f32,
122 );
123
124 let texture = self
125 .texture_creator
126 .create_texture_from_surface(surface)
127 .map_err(|e| e.to_string())
128 .unwrap();
129
130 self.canvas
131 .copy_ex(&texture, None, Some(dst_rect), 0.0, None, false, false)
132 .unwrap();
133
134 Ok(())
135 }
136}