1use std::path::Path;
2use std::rc::Rc;
3
4use glam::{vec2, Vec2};
5use image::io::Reader;
6use sdl2::pixels::PixelFormatEnum;
7use thiserror::Error;
8
9use crate::math::Rect;
10use crate::SdlError;
11
12use super::{with_canvas, Canvas, Drawable, Transform, Vertex};
13
14#[derive(Debug, Error)]
16pub enum LoadError {
17 #[error(transparent)]
19 Io(#[from] std::io::Error),
20 #[error(transparent)]
22 Decode(#[from] image::ImageError),
23 #[error(transparent)]
25 Renderer(#[from] SdlError),
26}
27
28#[derive(Debug, Default, Clone, Copy)]
32pub struct Origin(pub Vec2);
33
34impl Origin {
35 pub const TOP_LEFT: Self = Self(Vec2::ZERO);
37 pub const TOP_RIGHT: Self = Self(Vec2::X);
39 pub const BOTTOM_LEFT: Self = Self(Vec2::Y);
41 pub const BOTTOM_RIGHT: Self = Self(Vec2::ONE);
43 pub const CENTER: Self = Self(Vec2::splat(0.5));
45}
46
47#[derive(Default, Clone, Copy)]
49#[repr(u32)]
50pub enum ScaleMode {
51 #[default]
53 Nearest = 0,
54 Linear = 1,
56 }
59
60#[derive(Default)]
62pub struct Options {
63 pub scaling: Option<ScaleMode>,
66 pub origin: Origin,
68}
69
70impl From<ScaleMode> for Options {
71 fn from(scaling: ScaleMode) -> Self {
72 Self {
73 scaling: Some(scaling),
74 ..Default::default()
75 }
76 }
77}
78
79impl From<Origin> for Options {
80 fn from(origin: Origin) -> Self {
81 Self {
82 origin,
83 ..Default::default()
84 }
85 }
86}
87
88pub struct TextureData {
89 ptr: *mut sdl2_sys::SDL_Texture,
90 w: u32,
91 h: u32,
92}
93
94impl TextureData {
95 const fn empty() -> Self {
96 Self {
97 ptr: std::ptr::null_mut(),
98 w: 0,
99 h: 0,
100 }
101 }
102
103 fn from_image(img: image::DynamicImage, opts: &Options) -> Result<Self, LoadError> {
104 let w = img.width();
105 let h = img.height();
106 let (format, mut data) = if img.color().has_alpha() {
107 (PixelFormatEnum::RGBA32, img.into_rgba8().into_raw())
108 } else {
109 (PixelFormatEnum::RGB24, img.into_rgb8().into_raw())
110 };
111 let pitch = w * format.byte_size_per_pixel() as u32;
112
113 with_canvas(|canvas| unsafe {
114 let surface = sdl2_sys::SDL_CreateRGBSurfaceWithFormatFrom(
115 data.as_mut_ptr().cast(),
116 w as i32,
117 h as i32,
118 0,
119 pitch as i32,
120 format as u32,
121 );
122 if surface.is_null() {
123 return Err(SdlError::from_sdl())?;
124 }
125
126 let ptr = sdl2_sys::SDL_CreateTextureFromSurface(canvas.renderer(), surface);
127 if ptr.is_null() {
128 log::warn!("Failed to create a texture: {}", SdlError::from_sdl());
129 }
130
131 if let Some(scale) = opts.scaling {
132 let scale = std::mem::transmute::<ScaleMode, sdl2_sys::SDL_ScaleMode>(scale);
133 sdl2_sys::SDL_SetTextureScaleMode(ptr, scale);
134 }
135
136 Ok(Self { ptr, w, h })
137 })
138 }
139
140 pub const fn raw(&self) -> *mut sdl2_sys::SDL_Texture {
141 self.ptr
142 }
143}
144
145impl Drop for TextureData {
146 fn drop(&mut self) {
147 unsafe { sdl2_sys::SDL_DestroyTexture(self.ptr) }
148 }
149}
150
151#[must_use]
153#[derive(Clone)]
154pub struct Texture {
155 data: Rc<TextureData>,
156 origin: Vec2,
157}
158
159impl Texture {
160 pub fn empty() -> Self {
162 let data = Rc::new(TextureData::empty());
163 let origin = Vec2::ZERO;
164 Self { data, origin }
165 }
166
167 pub fn load(path: impl AsRef<Path>) -> Self {
169 Self::load_with(path, Options::default())
170 }
171
172 pub fn load_with(path: impl AsRef<Path>, options: impl Into<Options>) -> Self {
184 Self::try_load(path.as_ref(), options)
185 .inspect_err(|e| log::error!("Failed to load {}: {e}", path.as_ref().display()))
186 .unwrap_or_else(|_| Self::empty())
187 }
188
189 pub fn try_load(
191 path: impl AsRef<Path>,
192 options: impl Into<Options>,
193 ) -> Result<Self, LoadError> {
194 Self::from_image(Reader::open(path.as_ref())?.decode()?, options.into())
195 }
196
197 pub fn from_image(
199 img: image::DynamicImage,
200 options: impl Into<Options>,
201 ) -> Result<Self, LoadError> {
202 let options = options.into();
203 let origin = options.origin.0;
204 let data = Rc::new(TextureData::from_image(img, &options)?);
205 Ok(Self { data, origin })
206 }
207
208 pub fn slice(&self, rect: Rect) -> TextureSlice {
210 let texture = self.clone();
211 TextureSlice { texture, rect }
212 }
213
214 pub const fn with_origin(mut self, origin: Origin) -> Self {
216 self.origin = origin.0;
217 self
218 }
219
220 #[must_use]
222 pub fn width(&self) -> u32 {
223 self.data.w
224 }
225
226 #[must_use]
228 pub fn height(&self) -> u32 {
229 self.data.h
230 }
231
232 pub(crate) fn raw(&self) -> *mut sdl2_sys::SDL_Texture {
233 self.data.raw()
234 }
235}
236
237#[must_use]
239#[derive(Clone)]
240pub struct TextureSlice {
241 texture: Texture,
242 rect: Rect,
243}
244
245const QUAD_VERTS: [Vec2; 4] = [vec2(0., 0.), vec2(1., 0.), vec2(0., 1.), vec2(1., 1.)];
246const QUAD_IDX: [i32; 6] = [0, 1, 2, 2, 1, 3];
247
248impl Drawable for Texture {
249 fn draw(&self, canvas: &mut Canvas, transform: Transform) {
250 let size = vec2(self.data.w as f32, self.data.h as f32);
251 let transform = transform.scale(size);
252 let verts =
253 QUAD_VERTS.map(|p| Vertex::from_xy_uv(transform.transform_point(p - self.origin), p));
254
255 canvas.draw_geometry(self, &verts, Some(&QUAD_IDX));
256 }
257}
258
259impl Drawable for TextureSlice {
260 fn draw(&self, canvas: &mut Canvas, transform: Transform) {
261 let data = &self.texture.data;
262 let origin = self.texture.origin;
263
264 let size = vec2(self.rect.w as f32, self.rect.h as f32);
265 let uv = vec2(
266 self.rect.x as f32 / data.w as f32,
267 self.rect.y as f32 / data.h as f32,
268 );
269 let uv_size = vec2(
270 self.rect.w as f32 / data.w as f32,
271 self.rect.h as f32 / data.h as f32,
272 );
273 let transform = transform.scale(size);
274
275 let verts = QUAD_VERTS
276 .map(|p| Vertex::from_xy_uv(transform.transform_point(p - origin), p * uv_size + uv));
277
278 canvas.draw_geometry(&self.texture, &verts, Some(&QUAD_IDX));
279 }
280}