pix_engine/renderer/
sdl.rs

1//! SDL Renderer
2
3use crate::{
4    error::{Error, Result},
5    gui::theme::{FontId, FontSrc},
6    prelude::*,
7    renderer::{RendererSettings, Rendering},
8};
9use anyhow::{anyhow, Context};
10use log::{debug, warn};
11use lru::LruCache;
12use once_cell::sync::Lazy;
13use sdl2::{
14    audio::{AudioQueue, AudioSpecDesired},
15    controller::GameController,
16    gfx::primitives::{DrawRenderer, ToColor},
17    mouse::{Cursor, SystemCursor},
18    pixels::{Color as SdlColor, PixelFormatEnum as SdlPixelFormat},
19    rect::{Point as SdlPoint, Rect as SdlRect},
20    render::{BlendMode as SdlBlendMode, Canvas, TextureQuery},
21    rwops::RWops,
22    ttf::{Font as SdlFont, FontStyle as SdlFontStyle, Sdl2TtfContext},
23    video::Window,
24    EventPump, GameControllerSubsystem, Sdl,
25};
26use std::{collections::HashMap, fmt};
27use texture::RendererTexture;
28use window::{TextCacheKey, WindowCanvas};
29
30#[allow(clippy::expect_used)]
31static TTF: Lazy<Sdl2TtfContext> = Lazy::new(|| sdl2::ttf::init().expect("sdl2_ttf initialized"));
32
33pub use audio::{AudioDevice, AudioFormatNum};
34
35pub mod audio;
36mod event;
37mod texture;
38mod window;
39
40/// A SDL [Renderer] implementation.
41pub(crate) struct Renderer {
42    context: Sdl,
43    event_pump: EventPump,
44    audio_device: AudioQueue<f32>,
45    controller_subsys: GameControllerSubsystem,
46    controllers: HashMap<ControllerId, GameController>,
47    title: String,
48    settings: RendererSettings,
49    cursor: Option<Cursor>,
50    blend_mode: SdlBlendMode,
51    current_font: FontId,
52    font_size: u16,
53    font_style: SdlFontStyle,
54    primary_window_id: WindowId,
55    window_target: WindowId,
56    texture_target: Option<TextureId>,
57    windows: HashMap<WindowId, WindowCanvas>,
58    next_texture_id: usize,
59    font_data: LruCache<FontId, Font>,
60    loaded_fonts: LruCache<(FontId, u16), SdlFont<'static, 'static>>,
61}
62
63impl Renderer {
64    /// Update the current render target canvas.
65    fn update_canvas<F>(&mut self, f: F) -> Result<()>
66    where
67        F: FnOnce(&mut Canvas<Window>) -> Result<()>,
68    {
69        if let Some(texture_id) = self.texture_target {
70            let window = self
71                .windows
72                .values_mut()
73                .find(|w| w.textures.contains_key(&texture_id));
74            if let Some(window) = window {
75                // We ensured there's a valid texture above
76                let texture = window
77                    .textures
78                    .get(&texture_id)
79                    .ok_or_else(|| anyhow!(Error::InvalidTexture(texture_id)))?;
80                let mut result = Ok(());
81                window
82                    .canvas
83                    .with_texture_canvas(&mut texture.borrow_mut(), |canvas| {
84                        result = f(canvas);
85                    })
86                    .with_context(|| format!("failed to update texture target {texture_id}"))?;
87                result
88            } else {
89                Err(Error::InvalidTexture(texture_id).into())
90            }
91        } else {
92            f(self.canvas_mut()?)
93        }
94    }
95
96    /// Load font if family or size has not already been loaded. Returns `true` if a font was
97    /// loaded.
98    fn load_font(&mut self) -> Result<bool> {
99        let key = (self.current_font, self.font_size);
100        if self.loaded_fonts.contains(&key) {
101            return Ok(false);
102        }
103
104        let font_data = self
105            .font_data
106            .get(&self.current_font)
107            .ok_or_else(|| anyhow!("invalid current font"))?;
108        let loaded_font = match font_data.source() {
109            FontSrc::None => return Err(anyhow!("Must provide a font data source")),
110            FontSrc::Bytes(bytes) => {
111                let rwops = RWops::from_bytes(bytes).map_err(Error::Renderer)?;
112                TTF.load_font_from_rwops(rwops, self.font_size)
113                    .map_err(Error::Renderer)?
114            }
115            FontSrc::Path(ref path) => TTF
116                .load_font(path, self.font_size)
117                .map_err(Error::Renderer)?,
118        };
119        self.loaded_fonts.put(key, loaded_font);
120        Ok(true)
121    }
122
123    /// Returns a reference to the current SDL font.
124    #[inline]
125    fn font(&self) -> &SdlFont<'static, 'static> {
126        #[allow(clippy::expect_used)]
127        self.loaded_fonts
128            .peek(&(self.current_font, self.font_size))
129            .expect("valid font")
130    }
131
132    /// Returns a mutable reference the current SDL font.
133    #[inline]
134    fn font_mut(&mut self) -> &mut SdlFont<'static, 'static> {
135        #[allow(clippy::expect_used)]
136        self.loaded_fonts
137            .get_mut(&(self.current_font, self.font_size))
138            .expect("valid font")
139    }
140}
141
142impl Rendering for Renderer {
143    /// Initializes the `Sdl2Renderer` using the given settings and opens a new window.
144    #[inline]
145    fn new(mut s: RendererSettings) -> Result<Self> {
146        debug!("Initializing SDLRenderer");
147
148        let context = sdl2::init().map_err(Error::Renderer)?;
149        let event_pump = context.event_pump().map_err(Error::Renderer)?;
150
151        let title = s.title.clone();
152        let primary_window = WindowCanvas::new(&context, &mut s)?;
153        let cursor_result = Cursor::from_system(SystemCursor::Arrow).map_err(Error::Renderer);
154        let cursor = match cursor_result {
155            Ok(c) => Some(c),
156            Err(_) => None,
157        };
158        if let Ok(cursor) = Cursor::from_system(SystemCursor::Arrow) {
159            cursor.set();
160        }
161        let window_target = primary_window.id;
162        let mut windows = HashMap::new();
163        windows.insert(primary_window.id, primary_window);
164
165        // Set up Audio
166        let audio_subsys = context.audio().map_err(Error::Renderer)?;
167        let desired_spec = AudioSpecDesired {
168            freq: s.audio_sample_rate,
169            channels: s.audio_channels,
170            samples: s.audio_buffer_size,
171        };
172        let audio_device = audio_subsys
173            .open_queue(None, &desired_spec)
174            .map_err(Error::Renderer)?;
175        debug!("Loaded AudioDevice: {:?}", audio_device.spec());
176        let controller_subsys = context.game_controller().map_err(Error::Renderer)?;
177
178        let default_font = Font::default();
179        let current_font = default_font.id();
180        let mut font_data = LruCache::new(s.text_cache_size);
181        font_data.put(current_font, default_font);
182
183        let texture_cache_size = s.texture_cache_size;
184        let mut renderer = Self {
185            context,
186            event_pump,
187            audio_device,
188            controller_subsys,
189            controllers: HashMap::new(),
190            settings: s,
191            title,
192            cursor,
193            blend_mode: SdlBlendMode::None,
194            current_font,
195            font_size: 14,
196            font_style: SdlFontStyle::NORMAL,
197            primary_window_id: window_target,
198            window_target,
199            texture_target: None,
200            windows,
201            next_texture_id: 0,
202            font_data,
203            loaded_fonts: LruCache::new(texture_cache_size),
204        };
205        renderer.load_font()?;
206
207        Ok(renderer)
208    }
209
210    /// Clears the canvas to the current clear color.
211    #[inline]
212    fn clear(&mut self) -> Result<()> {
213        self.update_canvas(|canvas: &mut Canvas<_>| -> Result<()> {
214            canvas.clear();
215            Ok(())
216        })
217    }
218
219    /// Sets the color used by the renderer to draw to the current canvas.
220    #[inline]
221    fn set_draw_color(&mut self, color: Color) -> Result<()> {
222        self.update_canvas(|canvas: &mut Canvas<_>| -> Result<()> {
223            canvas.set_draw_color(color);
224            Ok(())
225        })
226    }
227
228    /// Sets the clip rect used by the renderer to draw to the current canvas.
229    #[inline]
230    fn clip(&mut self, rect: Option<Rect<i32>>) -> Result<()> {
231        self.update_canvas(|canvas: &mut Canvas<_>| -> Result<()> {
232            canvas.set_clip_rect(rect.map(Into::into));
233            Ok(())
234        })
235    }
236
237    /// Sets the blend mode used by the renderer to drawing.
238    #[inline]
239    fn blend_mode(&mut self, mode: BlendMode) {
240        self.blend_mode = mode.into();
241    }
242
243    /// Updates the canvas from the current back buffer.
244    #[inline]
245    fn present(&mut self) {
246        for window in self.windows.values_mut() {
247            window.canvas.present();
248        }
249    }
250
251    /// Set the rendering scale of the current canvas. Drawing coordinates are scaled by x/y
252    /// factors before being drawn to the canvas.
253    #[inline]
254    fn scale(&mut self, x: f32, y: f32) -> Result<()> {
255        self.update_canvas(|canvas: &mut Canvas<_>| -> Result<()> {
256            Ok(canvas.set_scale(x, y).map_err(Error::Renderer)?)
257        })
258    }
259
260    /// Set the font size for drawing to the current canvas.
261    #[inline]
262    fn font_size(&mut self, size: u32) -> Result<()> {
263        self.font_size = size as u16;
264        self.load_font()?;
265        Ok(())
266    }
267
268    /// Set the font style for drawing to the current canvas.
269    #[inline]
270    fn font_style(&mut self, style: FontStyle) {
271        let style = style.into();
272        if self.font_style != style {
273            self.font_style = style;
274            self.font_mut().set_style(style);
275        }
276    }
277
278    /// Set the font family for drawing to the current canvas.
279    #[inline]
280    fn font_family(&mut self, font: &Font) -> Result<()> {
281        self.current_font = font.id();
282        if !self.font_data.contains(&self.current_font) {
283            self.font_data.put(self.current_font, font.clone());
284        }
285        self.load_font()?;
286        Ok(())
287    }
288
289    /// Draw text to the current canvas.
290    #[inline]
291    fn text(
292        &mut self,
293        pos: Point<i32>,
294        text: &str,
295        wrap_width: Option<u32>,
296        angle: Option<f64>,
297        center: Option<Point<i32>>,
298        flipped: Option<Flipped>,
299        fill: Option<Color>,
300        outline: u16,
301    ) -> Result<(u32, u32)> {
302        if text.is_empty() {
303            return self.size_of(text, wrap_width);
304        }
305        if let Some(fill) = fill {
306            let window = self
307                .windows
308                .get_mut(&self.window_target)
309                .ok_or(Error::InvalidWindow(self.window_target))?;
310
311            let texture = {
312                // FIXME: Use default or return error
313                let font = self
314                    .loaded_fonts
315                    .get_mut(&(self.current_font, self.font_size))
316                    .ok_or_else(|| anyhow!("invalid current font"))?;
317                if font.get_outline_width() != outline {
318                    font.set_outline_width(outline);
319                }
320
321                let key = TextCacheKey::new(text, self.current_font, fill, self.font_size);
322                if !window.text_cache.contains(&key) {
323                    let surface = wrap_width
324                        .map_or_else(
325                            || font.render(text).blended(fill),
326                            |width| font.render(text).blended_wrapped(fill, width),
327                        )
328                        .context("invalid text")?;
329                    window.text_cache.put(
330                        key,
331                        RendererTexture::new(
332                            window
333                                .canvas
334                                .create_texture_from_surface(surface)
335                                .context("failed to create text surface")?,
336                        ),
337                    );
338                }
339
340                // SAFETY: We just checked or inserted a texture.
341                #[allow(clippy::expect_used)]
342                window.text_cache.get_mut(&key).expect("valid text cache")
343            };
344
345            let TextureQuery {
346                width, mut height, ..
347            } = texture.query();
348            let update = |canvas: &mut Canvas<_>| -> Result<()> {
349                let src = None;
350                let dst = Some(SdlRect::new(pos.x(), pos.y(), width, height));
351                let result = if angle.is_some() || center.is_some() || flipped.is_some() {
352                    let angle = angle.unwrap_or(0.0);
353                    let center = center.map(Into::into);
354                    let horizontal = matches!(flipped, Some(Flipped::Horizontal | Flipped::Both));
355                    let vertical = matches!(flipped, Some(Flipped::Vertical | Flipped::Both));
356                    canvas.copy_ex(texture, src, dst, angle, center, horizontal, vertical)
357                } else {
358                    canvas.copy(texture, src, dst)
359                };
360                Ok(result.map_err(Error::Renderer)?)
361            };
362
363            if let Some(texture_id) = self.texture_target {
364                if let Some(texture) = window.textures.get(&texture_id) {
365                    let mut result = Ok(());
366                    window
367                        .canvas
368                        .with_texture_canvas(&mut texture.borrow_mut(), |canvas| {
369                            result = update(canvas);
370                        })
371                        .with_context(|| format!("failed to update texture target {texture_id}"))?;
372                    result?;
373                } else {
374                    return Err(Error::InvalidTexture(texture_id).into());
375                }
376            } else {
377                update(&mut window.canvas)?;
378            }
379            if text.ends_with('\n') {
380                let font = self
381                    .loaded_fonts
382                    .get_mut(&(self.current_font, self.font_size))
383                    .ok_or_else(|| anyhow!("invalid current font"))?;
384                height += font.height() as u32;
385            }
386            Ok((width, height))
387        } else {
388            self.size_of(text, wrap_width)
389        }
390    }
391
392    /// Get clipboard text from the system clipboard.
393    #[inline]
394    fn clipboard_text(&self) -> String {
395        if let Ok(video) = self.context.video() {
396            video.clipboard().clipboard_text().unwrap_or_default()
397        } else {
398            String::default()
399        }
400    }
401
402    /// Set clipboard text to the system clipboard.
403    #[inline]
404    fn set_clipboard_text(&self, value: &str) -> Result<()> {
405        Ok(self
406            .context
407            .video()
408            .map_err(Error::Renderer)?
409            .clipboard()
410            .set_clipboard_text(value)
411            .map_err(Error::Renderer)?)
412    }
413
414    /// Open a URL in the default system browser.
415    #[inline]
416    fn open_url(&self, url: &str) -> Result<()> {
417        sdl2::url::open_url(url).context("invalid url")
418    }
419
420    /// Returns the rendered dimensions of the given text using the current font
421    /// as `(width, height)`.
422    #[inline]
423    fn size_of(&self, text: &str, wrap_width: Option<u32>) -> Result<(u32, u32)> {
424        let font = self.font();
425        if text.is_empty() {
426            return Ok((0, font.height() as u32));
427        }
428        let (width, mut height) = if let Some(width) = wrap_width {
429            let (width, height) = font
430                .render(text)
431                .blended_wrapped(Color::BLACK, width)
432                .context("invalid text")?
433                .size();
434            (width, height)
435        } else {
436            font.size_of(text)?
437        };
438        if text.ends_with('\n') {
439            height += font.height() as u32;
440        }
441        Ok((width, height))
442    }
443
444    /// Draw a pixel to the current canvas.
445    #[inline]
446    fn point(&mut self, p: Point<i32>, color: Color) -> Result<()> {
447        self.update_canvas(|canvas: &mut Canvas<_>| -> Result<()> {
448            let [x, y] = p.map(|v| v as i16);
449            Ok(canvas.pixel(x, y, color).map_err(Error::Renderer)?)
450        })
451    }
452
453    /// Draw a line to the current canvas.
454    #[inline]
455    fn line(&mut self, line: Line<i32>, smooth: bool, width: u8, color: Color) -> Result<()> {
456        self.update_canvas(|canvas: &mut Canvas<_>| -> Result<()> {
457            let [x1, y1] = line.start().map(|v| v as i16);
458            let [x2, y2] = line.end().map(|v| v as i16);
459            if width == 1 {
460                if y1 == y2 {
461                    canvas.hline(x1, x2, y1, color)
462                } else if x1 == x2 {
463                    canvas.vline(x1, y1, y2, color)
464                } else if smooth {
465                    canvas.aa_line(x1, y1, x2, y2, color)
466                } else {
467                    canvas.line(x1, y1, x2, y2, color)
468                }
469            } else {
470                canvas.thick_line(x1, y1, x2, y2, width, color)
471            }
472            .map_err(Error::Renderer)?;
473            Ok(())
474        })
475    }
476
477    /// Draw a cubic Bezier curve to the current canvas.
478    #[inline]
479    fn bezier<I>(&mut self, ps: I, detail: i32, stroke: Option<Color>) -> Result<()>
480    where
481        I: Iterator<Item = Point<i32>>,
482    {
483        self.update_canvas(|canvas: &mut Canvas<_>| -> Result<()> {
484            let (vx, vy): (Vec<i16>, Vec<i16>) = ps
485                .map(|p| -> (i16, i16) {
486                    let [x, y] = p.map(|v| v as i16);
487                    (x, y)
488                })
489                .unzip();
490            if let Some(stroke) = stroke {
491                canvas
492                    .bezier(&vx, &vy, detail, stroke)
493                    .map_err(Error::Renderer)?;
494            }
495            Ok(())
496        })
497    }
498
499    /// Draw a triangle to the current canvas.
500    #[inline]
501    fn triangle(
502        &mut self,
503        tri: Tri<i32>,
504        smooth: bool,
505        fill: Option<Color>,
506        stroke: Option<Color>,
507    ) -> Result<()> {
508        self.update_canvas(|canvas: &mut Canvas<_>| -> Result<()> {
509            let [x1, y1] = tri.p1().map(|v| v as i16);
510            let [x2, y2] = tri.p2().map(|v| v as i16);
511            let [x3, y3] = tri.p3().map(|v| v as i16);
512            if let Some(fill) = fill {
513                canvas
514                    .filled_trigon(x1, y1, x2, y2, x3, y3, fill)
515                    .map_err(Error::Renderer)?;
516            }
517            if let Some(stroke) = stroke {
518                if smooth {
519                    canvas.aa_trigon(x1, y1, x2, y2, x3, y3, stroke)
520                } else {
521                    canvas.trigon(x1, y1, x2, y2, x3, y3, stroke)
522                }
523                .map_err(Error::Renderer)?;
524            }
525            Ok(())
526        })
527    }
528
529    /// Draw a rectangle to the current canvas.
530    #[inline]
531    fn rect(
532        &mut self,
533        rect: Rect<i32>,
534        radius: Option<i32>,
535        fill: Option<Color>,
536        stroke: Option<Color>,
537    ) -> Result<()> {
538        self.update_canvas(|canvas: &mut Canvas<_>| -> Result<()> {
539            let [x, y, width, height] = rect.map(|v| v as i16);
540            if let Some(fill) = fill {
541                radius
542                    .map_or_else(
543                        || canvas.box_(x, y, x + width, y + height, fill),
544                        |radius| {
545                            let radius = radius as i16;
546                            canvas.rounded_box(x, y, x + width, y + height, radius, fill)
547                        },
548                    )
549                    .map_err(Error::Renderer)?;
550            }
551            if let Some(stroke) = stroke {
552                radius
553                    .map_or_else(
554                        // EXPL: SDL2_gfx renders this 1px smaller than it should.
555                        || canvas.rectangle(x, y, x + width + 1, y + height + 1, stroke),
556                        |radius| {
557                            let radius = radius as i16;
558                            canvas.rounded_rectangle(x, y, x + width, y + height, radius, stroke)
559                        },
560                    )
561                    .map_err(Error::Renderer)?;
562            }
563            Ok(())
564        })
565    }
566
567    /// Draw a quadrilateral to the current canvas.
568    #[inline]
569    fn quad(
570        &mut self,
571        quad: Quad<i32>,
572        smooth: bool,
573        fill: Option<Color>,
574        stroke: Option<Color>,
575    ) -> Result<()> {
576        self.update_canvas(|canvas: &mut Canvas<_>| -> Result<()> {
577            let [x1, y1] = quad.p1().map(|v| v as i16);
578            let [x2, y2] = quad.p2().map(|v| v as i16);
579            let [x3, y3] = quad.p3().map(|v| v as i16);
580            let [x4, y4] = quad.p4().map(|v| v as i16);
581            let vx = [x1, x2, x3, x4];
582            let vy = [y1, y2, y3, y4];
583            if let Some(fill) = fill {
584                canvas
585                    .filled_polygon(&vx, &vy, fill)
586                    .map_err(Error::Renderer)?;
587            }
588            if let Some(stroke) = stroke {
589                if smooth {
590                    canvas.aa_polygon(&vx, &vy, stroke)
591                } else {
592                    canvas.polygon(&vx, &vy, stroke)
593                }
594                .map_err(Error::Renderer)?;
595            }
596            Ok(())
597        })
598    }
599
600    /// Draw a polygon to the current canvas.
601    #[inline]
602    fn polygon<I>(
603        &mut self,
604        ps: I,
605        smooth: bool,
606        fill: Option<Color>,
607        stroke: Option<Color>,
608    ) -> Result<()>
609    where
610        I: Iterator<Item = Point<i32>>,
611    {
612        self.update_canvas(|canvas: &mut Canvas<_>| -> Result<()> {
613            let (vx, vy): (Vec<i16>, Vec<i16>) = ps
614                .map(|p| -> (i16, i16) {
615                    let [x, y] = p.map(|v| v as i16);
616                    (x, y)
617                })
618                .unzip();
619            if let Some(fill) = fill {
620                canvas
621                    .filled_polygon(&vx, &vy, fill)
622                    .map_err(Error::Renderer)?;
623            }
624            if let Some(stroke) = stroke {
625                if smooth {
626                    canvas.aa_polygon(&vx, &vy, stroke)
627                } else {
628                    canvas.polygon(&vx, &vy, stroke)
629                }
630                .map_err(Error::Renderer)?;
631            }
632            Ok(())
633        })
634    }
635
636    /// Draw a ellipse to the current canvas.
637    #[inline]
638    fn ellipse(
639        &mut self,
640        ellipse: Ellipse<i32>,
641        smooth: bool,
642        fill: Option<Color>,
643        stroke: Option<Color>,
644    ) -> Result<()> {
645        self.update_canvas(|canvas: &mut Canvas<_>| -> Result<()> {
646            let [x, y, width, height] = ellipse.map(|v| v as i16);
647            let rw = width / 2;
648            let rh = height / 2;
649            if let Some(fill) = fill {
650                if width == height {
651                    canvas.filled_circle(x, y, rw, fill)
652                } else {
653                    canvas.filled_ellipse(x, y, rw, rh, fill)
654                }
655                .map_err(Error::Renderer)?;
656            }
657            if let Some(stroke) = stroke {
658                if width == height {
659                    if smooth {
660                        canvas.aa_circle(x, y, rw, stroke)
661                    } else {
662                        canvas.circle(x, y, rw, stroke)
663                    }
664                } else if smooth {
665                    canvas.aa_ellipse(x, y, rw, rh, stroke)
666                } else {
667                    canvas.ellipse(x, y, rw, rh, stroke)
668                }
669                .map_err(Error::Renderer)?;
670            }
671            Ok(())
672        })
673    }
674
675    /// Draw an arc to the current canvas.
676    #[inline]
677    fn arc(
678        &mut self,
679        p: Point<i32>,
680        radius: i32,
681        start: i32,
682        end: i32,
683        mode: ArcMode,
684        fill: Option<Color>,
685        stroke: Option<Color>,
686    ) -> Result<()> {
687        self.update_canvas(|canvas: &mut Canvas<_>| -> Result<()> {
688            let [x, y] = p.map(|v| v as i16);
689            let radius = radius as i16;
690            let start = start as i16;
691            let end = end as i16;
692            match mode {
693                ArcMode::Default => {
694                    if let Some(stroke) = stroke {
695                        canvas
696                            .arc(x, y, radius, start, end, stroke)
697                            .map_err(Error::Renderer)?;
698                    }
699                }
700                ArcMode::Pie => {
701                    if let Some(fill) = fill {
702                        canvas
703                            .filled_pie(x, y, radius, start, end, fill)
704                            .map_err(Error::Renderer)?;
705                    }
706                    if let Some(stroke) = stroke {
707                        canvas
708                            .pie(x, y, radius, start, end, stroke)
709                            .map_err(Error::Renderer)?;
710                    }
711                }
712            }
713            Ok(())
714        })
715    }
716
717    /// Draw an image to the current canvas, optionally rotated about a `center`, flipped or tinted
718    #[inline]
719    fn image(
720        &mut self,
721        img: &Image,
722        src: Option<Rect<i32>>,
723        dst: Option<Rect<i32>>,
724        angle: f64,
725        center: Option<Point<i32>>,
726        flipped: Option<Flipped>,
727        tint: Option<Color>,
728    ) -> Result<()> {
729        let window = self
730            .windows
731            .get_mut(&self.window_target)
732            .ok_or(Error::InvalidWindow(self.window_target))?;
733        let texture = {
734            let key: *const Image = img;
735            if !window.image_cache.contains(&key) {
736                window.image_cache.put(
737                    key,
738                    RendererTexture::new(
739                        window
740                            .canvas
741                            .create_texture_static(
742                                Some(img.format().into()),
743                                img.width(),
744                                img.height(),
745                            )
746                            .context("failed to create image texture")?,
747                    ),
748                );
749            }
750            // SAFETY: We just checked or inserted a texture.
751            #[allow(clippy::expect_used)]
752            window.image_cache.get_mut(&key).expect("valid image cache")
753        };
754        let [r, g, b, a] = tint.map_or([255; 4], |t| t.channels());
755        texture.set_color_mod(r, g, b);
756        texture.set_alpha_mod(a);
757        texture.set_blend_mode(self.blend_mode);
758        texture
759            .update(
760                None,
761                img.as_bytes(),
762                img.format().channels() * img.width() as usize,
763            )
764            .context("failed to update image texture")?;
765
766        let update = |canvas: &mut Canvas<_>| -> Result<()> {
767            let src = src.map(Into::into);
768            let dst = dst.map(Into::into);
769            if angle > 0.0 || center.is_some() || flipped.is_some() {
770                let center = center.map(Into::into);
771                let horizontal = matches!(flipped, Some(Flipped::Horizontal | Flipped::Both));
772                let vertical = matches!(flipped, Some(Flipped::Vertical | Flipped::Both));
773                canvas.copy_ex(texture, src, dst, angle, center, horizontal, vertical)
774            } else {
775                canvas.copy(texture, src, dst)
776            }
777            .map_err(Error::Renderer)?;
778            Ok(())
779        };
780
781        if let Some(texture_id) = self.texture_target {
782            if let Some(texture) = window.textures.get(&texture_id) {
783                let mut result = Ok(());
784                window
785                    .canvas
786                    .with_texture_canvas(&mut texture.borrow_mut(), |canvas| {
787                        result = update(canvas);
788                    })
789                    .with_context(|| format!("failed to update texture target {texture_id}"))?;
790                result?;
791            } else {
792                return Err(Error::InvalidTexture(texture_id).into());
793            }
794        } else {
795            update(&mut window.canvas)?;
796        }
797
798        Ok(())
799    }
800
801    /// Return the current rendered target pixels as an array of bytes.
802    #[inline]
803    fn to_bytes(&mut self) -> Result<Vec<u8>> {
804        if let Some(texture_id) = self.texture_target {
805            let window = self
806                .windows
807                .values_mut()
808                .find(|w| w.textures.contains_key(&texture_id));
809            if let Some(window) = window {
810                // We ensured there's a valid texture above
811                let texture = window
812                    .textures
813                    .get(&texture_id)
814                    .ok_or_else(|| anyhow!(Error::InvalidTexture(texture_id)))?;
815                let mut result = Ok(vec![]);
816                window
817                    .canvas
818                    .with_texture_canvas(&mut texture.borrow_mut(), |canvas| {
819                        result = canvas.read_pixels(None, SdlPixelFormat::RGBA32);
820                    })
821                    .with_context(|| format!("failed to read texture target {texture_id}"))?;
822                Ok(result.map_err(Error::Renderer)?)
823            } else {
824                Err(Error::InvalidTexture(texture_id).into())
825            }
826        } else {
827            Ok(self
828                .canvas()?
829                .read_pixels(None, SdlPixelFormat::RGBA32)
830                .map_err(Error::Renderer)?)
831        }
832    }
833
834    /// Connect a controller with the given joystick index to start receiving events.
835    fn open_controller(&mut self, controller_id: ControllerId) -> Result<()> {
836        let joystick_index = *controller_id;
837        if self.controller_subsys.is_game_controller(joystick_index) {
838            self.controllers
839                .insert(controller_id, self.controller_subsys.open(joystick_index)?);
840        } else {
841            warn!("Joystick {} is not a game controller. Generic joysticks are currently unsupported.", joystick_index);
842        }
843        Ok(())
844    }
845
846    /// Disconnect a controller with the given joystick index to stop receiving events.
847    fn close_controller(&mut self, controller_id: ControllerId) {
848        self.controllers.remove(&controller_id);
849    }
850}
851
852impl fmt::Debug for Renderer {
853    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
854        f.debug_struct("Renderer")
855            .field(
856                "audio_device",
857                &format_args!(
858                    "{{ spec: {:?}, status: {:?}, queue: {:?} }}",
859                    self.audio_device.spec(),
860                    self.audio_device.status(),
861                    self.audio_device.size()
862                ),
863            )
864            .field("title", &self.title)
865            .field("settings", &self.settings)
866            .field("blend_mode", &self.blend_mode)
867            .field(
868                "current_font",
869                &self.font_data.peek(&self.current_font).map(Font::name),
870            )
871            .field("font_size", &self.font_size)
872            .field("font_style", &self.font_style)
873            .field("window_target", &self.texture_target)
874            .field("texture_target", &self.texture_target)
875            .field("windows", &self.windows)
876            .field("next_texture_id", &self.next_texture_id)
877            .field("font_data", &self.font_data)
878            .field("loaded_fonts", &self.loaded_fonts)
879            .finish_non_exhaustive()
880    }
881}
882
883/*
884 * Type Conversions
885 */
886
887#[doc(hidden)]
888impl ToColor for Color {
889    /// Convert [Color] to tuple of `(r, g, b, a)`.
890    fn as_rgba(&self) -> (u8, u8, u8, u8) {
891        let [r, g, b, a] = self.channels();
892        (r, g, b, a)
893    }
894}
895
896#[doc(hidden)]
897impl From<Color> for SdlColor {
898    /// Convert [Color] to [`SdlColor`].
899    fn from(color: Color) -> Self {
900        let [r, g, b, a] = color.channels();
901        Self::RGBA(r, g, b, a)
902    }
903}
904
905#[doc(hidden)]
906impl From<FontStyle> for SdlFontStyle {
907    /// Convert [`FontStyle`] to [`SdlFontStyle`].
908    fn from(style: FontStyle) -> Self {
909        #[allow(clippy::expect_used)]
910        Self::from_bits(style.bits()).expect("valid FontStyle")
911    }
912}
913
914#[doc(hidden)]
915impl From<Rect<i32>> for SdlRect {
916    /// Convert [`Rect<i32>`] to [`SdlRect`].
917    fn from(rect: Rect<i32>) -> Self {
918        Self::new(
919            rect.x(),
920            rect.y(),
921            rect.width() as u32,
922            rect.height() as u32,
923        )
924    }
925}
926
927#[doc(hidden)]
928impl From<SdlRect> for Rect<i32> {
929    /// Convert [`Rect<i32>`] to [`SdlRect`].
930    fn from(rect: SdlRect) -> Self {
931        Self::new(
932            rect.x(),
933            rect.y(),
934            rect.width() as i32,
935            rect.height() as i32,
936        )
937    }
938}
939
940#[doc(hidden)]
941impl From<&Rect<i32>> for SdlRect {
942    /// Convert &[`Rect<i32>`] to [`SdlRect`].
943    fn from(rect: &Rect<i32>) -> Self {
944        Self::new(
945            rect.x(),
946            rect.y(),
947            rect.width() as u32,
948            rect.height() as u32,
949        )
950    }
951}
952
953#[doc(hidden)]
954impl From<Point<i32>> for SdlPoint {
955    /// Convert [`Point<i32>`] to [`SdlPoint`].
956    fn from(p: Point<i32>) -> Self {
957        Self::new(p.x(), p.y())
958    }
959}
960
961#[doc(hidden)]
962impl From<&Point<i32>> for SdlPoint {
963    /// Convert &[`Point<i32>`] to [`SdlPoint`].
964    fn from(p: &Point<i32>) -> Self {
965        Self::new(p.x(), p.y())
966    }
967}
968
969#[doc(hidden)]
970impl From<BlendMode> for SdlBlendMode {
971    /// Convert [`BlendMode`] to [`SdlBlendMode`].
972    fn from(mode: BlendMode) -> Self {
973        match mode {
974            BlendMode::None => Self::None,
975            BlendMode::Blend => Self::Blend,
976            BlendMode::Add => Self::Add,
977            BlendMode::Mod => Self::Mod,
978        }
979    }
980}
981
982#[doc(hidden)]
983impl From<PixelFormat> for SdlPixelFormat {
984    /// Convert [`PixelFormat`] to [`SdlPixelFormat`].
985    fn from(format: PixelFormat) -> Self {
986        match format {
987            PixelFormat::Rgb => Self::RGB24,
988            PixelFormat::Rgba => Self::RGBA32,
989        }
990    }
991}