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