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