1use std::fmt;
2use std::sync::Arc;
3
4use repose_core::locals::effective_density_scale;
5use repose_core::{
6 Color, ControlVisual, ControlVisualState, Dp, ImageAlignment, ImageFilter, ImageFit,
7 ImageHandle, ImageHandleGuard, ImagePaintStyle, ImageSourceRect, Modifier, Rect, RenderContext,
8 Scene, SceneNode, View,
9};
10use repose_ui::Box as UiBox;
11
12#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct SpriteImageFrameDef {
14 pub source_rect: ImageSourceRect,
15 pub origin: [f32; 2],
16 pub duration: f32,
17}
18
19#[derive(Clone, Debug, Default, PartialEq)]
20pub struct SpriteImageDef {
21 pub frames: Vec<SpriteImageFrameDef>,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum SpriteImageError {
26 EmptyDefinition,
27 InvalidTextureSize,
28 InvalidFrame(usize),
29 InvalidTiming(usize),
30 InvalidPixelBuffer,
31}
32
33impl fmt::Display for SpriteImageError {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 match self {
36 Self::EmptyDefinition => f.write_str("sprite image definition has no frames"),
37 Self::InvalidTextureSize => {
38 f.write_str("sprite image texture dimensions must be nonzero")
39 }
40 Self::InvalidFrame(index) => write!(f, "sprite image frame {index} is invalid"),
41 Self::InvalidTiming(index) => {
42 write!(f, "sprite image frame {index} has invalid timing")
43 }
44 Self::InvalidPixelBuffer => f.write_str("sprite image RGBA buffer has the wrong size"),
45 }
46 }
47}
48
49impl std::error::Error for SpriteImageError {}
50
51impl SpriteImageDef {
52 pub fn new(frames: Vec<SpriteImageFrameDef>) -> Self {
53 Self { frames }
54 }
55
56 pub fn single(frame_size: [u32; 2], origin: [f32; 2]) -> Self {
57 Self {
58 frames: vec![SpriteImageFrameDef {
59 source_rect: ImageSourceRect::new(0, 0, frame_size[0], frame_size[1]),
60 origin,
61 duration: 0.0,
62 }],
63 }
64 }
65
66 pub fn horizontal_strip(
67 frames: u32,
68 frame_size: [u32; 2],
69 origin: [f32; 2],
70 fps: f32,
71 ) -> Result<Self, SpriteImageError> {
72 let duration = if fps > 0.0 { 1.0 / fps } else { 0.0 };
73 if !fps.is_finite() || !duration.is_finite() {
74 return Err(SpriteImageError::InvalidTiming(0));
75 }
76 let frames = (0..frames)
77 .map(|frame| {
78 let x = frame.saturating_mul(frame_size[0]);
79 SpriteImageFrameDef {
80 source_rect: ImageSourceRect::new(x, 0, frame_size[0], frame_size[1]),
81 origin,
82 duration,
83 }
84 })
85 .collect();
86 Ok(Self { frames })
87 }
88}
89
90struct SpriteImageAssetInner {
91 guard: ImageHandleGuard,
92 texture_size: [u32; 2],
93 frames: Arc<[SpriteImageFrameDef]>,
94}
95
96#[derive(Clone)]
97pub struct SpriteImageAsset {
98 inner: Arc<SpriteImageAssetInner>,
99}
100
101impl fmt::Debug for SpriteImageAsset {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 f.debug_struct("SpriteImageAsset")
104 .field("handle", &self.handle())
105 .field("texture_size", &self.texture_size())
106 .field("frame_count", &self.frame_count())
107 .finish()
108 }
109}
110
111impl SpriteImageAsset {
112 pub fn from_encoded(
113 context: &RenderContext,
114 bytes: impl Into<Vec<u8>>,
115 texture_size: [u32; 2],
116 definition: SpriteImageDef,
117 srgb: bool,
118 ) -> Result<Self, SpriteImageError> {
119 Self::validate(texture_size, &definition)?;
120 let guard = ImageHandleGuard::new(context);
121 context.set_image_encoded(*guard, bytes.into(), srgb);
122 Ok(Self::from_parts(guard, texture_size, definition))
123 }
124
125 pub fn from_rgba8(
126 context: &RenderContext,
127 texture_size: [u32; 2],
128 rgba: Vec<u8>,
129 definition: SpriteImageDef,
130 srgb: bool,
131 ) -> Result<Self, SpriteImageError> {
132 Self::validate(texture_size, &definition)?;
133 let expected = usize::try_from(texture_size[0])
134 .ok()
135 .and_then(|width| {
136 usize::try_from(texture_size[1])
137 .ok()
138 .and_then(|height| width.checked_mul(height))
139 })
140 .and_then(|pixels| pixels.checked_mul(4))
141 .ok_or(SpriteImageError::InvalidPixelBuffer)?;
142 if rgba.len() != expected {
143 return Err(SpriteImageError::InvalidPixelBuffer);
144 }
145 let guard = ImageHandleGuard::new(context);
146 context.set_image_rgba8(*guard, texture_size[0], texture_size[1], rgba, srgb);
147 Ok(Self::from_parts(guard, texture_size, definition))
148 }
149
150 fn from_parts(
151 guard: ImageHandleGuard,
152 texture_size: [u32; 2],
153 definition: SpriteImageDef,
154 ) -> Self {
155 Self {
156 inner: Arc::new(SpriteImageAssetInner {
157 guard,
158 texture_size,
159 frames: definition.frames.into(),
160 }),
161 }
162 }
163
164 fn validate(
165 texture_size: [u32; 2],
166 definition: &SpriteImageDef,
167 ) -> Result<(), SpriteImageError> {
168 if texture_size[0] == 0 || texture_size[1] == 0 {
169 return Err(SpriteImageError::InvalidTextureSize);
170 }
171 if definition.frames.is_empty() {
172 return Err(SpriteImageError::EmptyDefinition);
173 }
174 for (index, frame) in definition.frames.iter().enumerate() {
175 let Some(right) = frame.source_rect.x.checked_add(frame.source_rect.width) else {
176 return Err(SpriteImageError::InvalidFrame(index));
177 };
178 let Some(bottom) = frame.source_rect.y.checked_add(frame.source_rect.height) else {
179 return Err(SpriteImageError::InvalidFrame(index));
180 };
181 if frame.source_rect.is_empty()
182 || right > texture_size[0]
183 || bottom > texture_size[1]
184 || !frame.origin[0].is_finite()
185 || !frame.origin[1].is_finite()
186 {
187 return Err(SpriteImageError::InvalidFrame(index));
188 }
189 if !frame.duration.is_finite() || frame.duration < 0.0 {
190 return Err(SpriteImageError::InvalidTiming(index));
191 }
192 }
193 Ok(())
194 }
195
196 pub fn handle(&self) -> ImageHandle {
197 *self.inner.guard
198 }
199
200 pub fn texture_size(&self) -> [u32; 2] {
201 self.inner.texture_size
202 }
203
204 pub fn frame_count(&self) -> usize {
205 self.inner.frames.len()
206 }
207
208 pub fn frame(&self, frame: i32) -> Option<SpriteImage> {
209 if self.inner.frames.is_empty() {
210 return None;
211 }
212 let index = frame.rem_euclid(self.inner.frames.len() as i32) as usize;
213 let def = self.inner.frames[index];
214 Some(SpriteImage {
215 asset: self.clone(),
216 source_rect: def.source_rect,
217 size: [def.source_rect.width as f32, def.source_rect.height as f32],
218 origin: def.origin,
219 duration: def.duration,
220 })
221 }
222}
223
224#[derive(Clone)]
225pub struct SpriteImage {
226 asset: SpriteImageAsset,
227 source_rect: ImageSourceRect,
228 size: [f32; 2],
229 origin: [f32; 2],
230 duration: f32,
231}
232
233impl fmt::Debug for SpriteImage {
234 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235 f.debug_struct("SpriteImage")
236 .field("handle", &self.handle())
237 .field("source_rect", &self.source_rect)
238 .field("size", &self.size)
239 .field("origin", &self.origin)
240 .field("duration", &self.duration)
241 .finish()
242 }
243}
244
245impl SpriteImage {
246 pub fn handle(&self) -> ImageHandle {
247 self.asset.handle()
248 }
249
250 pub fn source_rect(&self) -> ImageSourceRect {
251 self.source_rect
252 }
253
254 pub fn size(&self) -> [f32; 2] {
255 self.size
256 }
257
258 pub fn origin(&self) -> [f32; 2] {
259 self.origin
260 }
261
262 pub fn anchor(&self) -> [f32; 2] {
263 if self.size[0] <= 0.0 || self.size[1] <= 0.0 {
264 [0.0, 0.0]
265 } else {
266 [self.origin[0] / self.size[0], self.origin[1] / self.size[1]]
267 }
268 }
269
270 pub fn duration(&self) -> f32 {
271 self.duration
272 }
273
274 pub fn view(&self, modifier: Modifier) -> View {
275 let has_size = modifier.size.is_some()
276 || modifier.width.is_some()
277 || modifier.height.is_some()
278 || modifier.fill_max.is_some()
279 || modifier.fill_max_w.is_some()
280 || modifier.fill_max_h.is_some();
281 let modifier = if has_size {
282 modifier
283 } else {
284 modifier.size(Dp(self.size[0]), Dp(self.size[1]))
285 };
286 let visual = self.control_visual(Color::WHITE);
287 UiBox(modifier.painter(move |scene, rect, alpha| {
288 visual.paint(
289 scene,
290 rect,
291 ControlVisualState {
292 alpha,
293 ..ControlVisualState::default()
294 },
295 );
296 }))
297 }
298
299 pub fn control_visual(&self, tint: Color) -> ControlVisual {
300 let _asset = self.asset.clone();
301 let handle = self.handle();
302 let source = self.source_rect;
303 ControlVisual::custom(move |scene, rect, state| {
304 let _ = &_asset;
305 let alpha = state.alpha.clamp(0.0, 1.0);
306 let tint = Color(tint.0, tint.1, tint.2, (tint.3 as f32 * alpha) as u8);
307 scene.nodes.push(SceneNode::Image {
308 rect,
309 handle,
310 tint,
311 style: ImagePaintStyle {
312 fit: ImageFit::FillBounds,
313 filter: ImageFilter::Nearest,
314 source_rect: Some(source),
315 alignment: ImageAlignment::Center,
316 },
317 });
318 })
319 }
320
321 pub fn rotated_control_visual(&self, tint: Color, quarter_turns: i32) -> ControlVisual {
322 let quarter_turns = quarter_turns.rem_euclid(4);
323 if quarter_turns == 0 {
324 return self.control_visual(tint);
325 }
326 let _asset = self.asset.clone();
327 let handle = self.handle();
328 let source = self.source_rect;
329 ControlVisual::custom(
330 move |scene: &mut Scene, rect: Rect, state: ControlVisualState| {
331 let _ = &_asset;
332 let angle = quarter_turns as f32 * std::f32::consts::FRAC_PI_2;
333 let (sin, cos) = angle.sin_cos();
334 let center_x = rect.x + rect.w * 0.5;
335 let center_y = rect.y + rect.h * 0.5;
336 let transform = repose_core::Transform {
337 translate_x: center_x - (cos * center_x - sin * center_y),
338 translate_y: center_y - (sin * center_x + cos * center_y),
339 rotate: angle,
340 origin_x: 0.0,
341 origin_y: 0.0,
342 ..repose_core::Transform::identity()
343 };
344 let image_rect = if quarter_turns % 2 == 1 {
345 Rect {
346 x: center_x - rect.h * 0.5,
347 y: center_y - rect.w * 0.5,
348 w: rect.h,
349 h: rect.w,
350 }
351 } else {
352 rect
353 };
354 let alpha = state.alpha.clamp(0.0, 1.0);
355 let tint = Color(tint.0, tint.1, tint.2, (tint.3 as f32 * alpha) as u8);
356 scene.nodes.push(SceneNode::PushTransform { transform });
357 scene.nodes.push(SceneNode::Image {
358 rect: image_rect,
359 handle,
360 tint,
361 style: ImagePaintStyle {
362 fit: ImageFit::FillBounds,
363 filter: ImageFilter::Nearest,
364 source_rect: Some(source),
365 alignment: ImageAlignment::Center,
366 },
367 });
368 scene.nodes.push(SceneNode::PopTransform);
369 },
370 )
371 }
372
373 pub fn prefix_control_visual(&self, tint: Color) -> ControlVisual {
374 let _asset = self.asset.clone();
375 let handle = self.handle();
376 let source = self.source_rect;
377 let natural_width = self.size[0].max(1.0);
378 ControlVisual::custom(
379 move |scene: &mut Scene, rect: Rect, state: ControlVisualState| {
380 let _ = &_asset;
381 let density = effective_density_scale().max(1e-6);
382 let logical_width = rect.w / density;
383 let visible = if logical_width.is_finite() && logical_width > 0.0 {
384 ((logical_width / natural_width).clamp(0.0, 1.0) * source.width as f32)
385 .floor()
386 .max(1.0) as u32
387 } else {
388 source.width
389 };
390 let visible = visible.min(source.width);
391 let alpha = state.alpha.clamp(0.0, 1.0);
392 let tint = Color(tint.0, tint.1, tint.2, (tint.3 as f32 * alpha) as u8);
393 scene.nodes.push(SceneNode::Image {
394 rect,
395 handle,
396 tint,
397 style: ImagePaintStyle {
398 fit: ImageFit::FillBounds,
399 filter: ImageFilter::Nearest,
400 source_rect: Some(ImageSourceRect::new(
401 source.x,
402 source.y,
403 visible,
404 source.height,
405 )),
406 alignment: ImageAlignment::Center,
407 },
408 });
409 },
410 )
411 }
412
413 pub fn subimage(&self, source_rect: ImageSourceRect) -> Option<Self> {
414 let right = source_rect.x.checked_add(source_rect.width)?;
415 let bottom = source_rect.y.checked_add(source_rect.height)?;
416 let [width, height] = self.asset.texture_size();
417 if source_rect.is_empty() || right > width || bottom > height {
418 return None;
419 }
420 Some(Self {
421 asset: self.asset.clone(),
422 source_rect,
423 size: [source_rect.width as f32, source_rect.height as f32],
424 origin: self.origin,
425 duration: self.duration,
426 })
427 }
428
429 pub fn relative_subimage(&self, x: u32, y: u32, width: u32, height: u32) -> Option<Self> {
430 let source_x = self.source_rect.x.checked_add(x)?;
431 let source_y = self.source_rect.y.checked_add(y)?;
432 self.subimage(ImageSourceRect::new(source_x, source_y, width, height))
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use repose_core::{ControlVisualState, RenderCommand};
440
441 #[test]
442 fn sprite_frame_becomes_source_rect_control_visual() {
443 let context = RenderContext::new();
444 let definition = SpriteImageDef::horizontal_strip(2, [2, 2], [1.0, 1.0], 8.0)
445 .expect("strip should be valid");
446 let (handle, visual) = {
447 let asset = SpriteImageAsset::from_rgba8(
448 &context,
449 [4, 2],
450 vec![255; 4 * 2 * 4],
451 definition,
452 true,
453 )
454 .expect("asset should be valid");
455 let frame = asset.frame(1).expect("frame should exist");
456 (frame.handle(), frame.control_visual(Color::WHITE))
457 };
458 let _ = context.drain();
459 let mut scene = Scene::default();
460 visual.paint(
461 &mut scene,
462 Rect {
463 x: 3.0,
464 y: 5.0,
465 w: 20.0,
466 h: 10.0,
467 },
468 ControlVisualState::default(),
469 );
470 let Some(SceneNode::Image {
471 rect,
472 style:
473 ImagePaintStyle {
474 source_rect,
475 filter,
476 ..
477 },
478 ..
479 }) = scene.nodes.first()
480 else {
481 panic!("expected image node");
482 };
483 assert_eq!(rect.w, 20.0);
484 assert_eq!(*source_rect, Some(ImageSourceRect::new(2, 0, 2, 2)));
485 assert_eq!(*filter, ImageFilter::Nearest);
486 assert!(context.drain().is_empty());
487 drop(visual);
488 assert!(matches!(
489 context.drain().as_slice(),
490 [RenderCommand::RemoveImage { handle: removed }] if *removed == handle
491 ));
492 }
493
494 #[test]
495 fn rotated_visual_swaps_quarter_turn_rect() {
496 let context = RenderContext::new();
497 let definition = SpriteImageDef::single([8, 2], [4.0, 1.0]);
498 let visual =
499 SpriteImageAsset::from_rgba8(&context, [8, 2], vec![255; 8 * 2 * 4], definition, true)
500 .expect("asset should be valid")
501 .frame(0)
502 .expect("frame should exist")
503 .rotated_control_visual(Color::WHITE, 1);
504 let mut scene = Scene::default();
505 visual.paint(
506 &mut scene,
507 Rect {
508 x: 10.0,
509 y: 20.0,
510 w: 20.0,
511 h: 80.0,
512 },
513 ControlVisualState::default(),
514 );
515 assert_eq!(scene.nodes.len(), 3);
516 assert!(matches!(
517 scene.nodes[1],
518 SceneNode::Image { rect, .. } if rect == Rect { x: -20.0, y: 50.0, w: 80.0, h: 20.0 }
519 ));
520 assert!(matches!(&scene.nodes[2], SceneNode::PopTransform));
521 }
522
523 #[test]
524 fn prefix_visual_uses_logical_control_width() {
525 let context = RenderContext::new();
526 let definition = SpriteImageDef::horizontal_strip(1, [2, 2], [1.0, 1.0], 8.0)
527 .expect("strip should be valid");
528 let visual =
529 SpriteImageAsset::from_rgba8(&context, [2, 2], vec![255; 2 * 2 * 4], definition, true)
530 .expect("asset should be valid")
531 .frame(0)
532 .expect("frame should exist")
533 .prefix_control_visual(Color::WHITE);
534 let mut scene = Scene::default();
535 repose_core::locals::with_density(repose_core::locals::Density { scale: 2.0 }, || {
536 visual.paint(
537 &mut scene,
538 Rect {
539 x: 0.0,
540 y: 0.0,
541 w: 2.0,
542 h: 2.0,
543 },
544 ControlVisualState::default(),
545 );
546 });
547 let Some(SceneNode::Image {
548 style: ImagePaintStyle { source_rect, .. },
549 ..
550 }) = scene.nodes.first()
551 else {
552 panic!("expected image node");
553 };
554 assert_eq!(*source_rect, Some(ImageSourceRect::new(0, 0, 1, 2)));
555 }
556}