1use bevy::{
4 app::PluginGroupBuilder,
5 prelude::*,
6 render::{
7 render_resource::{Extent3d, TextureDescriptor, TextureDimension, TextureUsages},
8 texture::ImageSampler,
9 },
10 window::PrimaryWindow,
11};
12
13use crate::prelude::Pixel;
14
15#[derive(Component, Default, Clone, Copy, Debug, PartialEq)]
20pub struct PixelBuffer {
21 pub size: PixelBufferSize,
23 pub fill: Fill,
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub struct PixelBufferSize {
30 pub size: UVec2,
32 pub pixel_size: UVec2,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq)]
38pub struct Fill {
39 pub(crate) kind: FillKind,
40 pub(crate) stretch: bool,
41 pub(crate) multiple: u32,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq)]
46pub enum FillKind {
47 None,
49 Window,
51 Custom(Vec2),
53}
54
55impl Default for Fill {
56 fn default() -> Self {
57 Self {
58 kind: FillKind::None,
59 stretch: false,
60 multiple: 1,
61 }
62 }
63}
64
65impl Fill {
66 pub fn none() -> Self {
68 Self {
69 kind: FillKind::None,
70 ..Default::default()
71 }
72 }
73
74 pub fn stretch() -> Self {
85 Self {
86 stretch: true,
87 ..Default::default()
88 }
89 }
90
91 pub fn window() -> Self {
93 Self {
94 kind: FillKind::Window,
95 ..Default::default()
96 }
97 }
98
99 pub fn custom(s: impl Into<Vec2>) -> Self {
101 Self {
102 kind: FillKind::Custom(s.into()),
103 ..Default::default()
104 }
105 }
106
107 pub fn with_stretch(mut self, stretch: bool) -> Self {
109 self.stretch = stretch;
110 self
111 }
112
113 pub fn with_scaling_multiple(mut self, multiple: u32) -> Self {
117 self.multiple = multiple;
118 self
119 }
120}
121
122impl From<FillKind> for Fill {
123 fn from(f: FillKind) -> Self {
124 Self {
125 kind: f,
126 ..Default::default()
127 }
128 }
129}
130
131impl From<(u32, u32)> for PixelBufferSize {
132 fn from(v: (u32, u32)) -> Self {
133 Self {
134 size: v.into(),
135 ..Default::default()
136 }
137 }
138}
139
140impl From<((u32, u32), (u32, u32))> for PixelBufferSize {
141 fn from((size, pixel_size): ((u32, u32), (u32, u32))) -> Self {
142 Self {
143 size: size.into(),
144 pixel_size: pixel_size.into(),
145 }
146 }
147}
148
149impl PixelBufferSize {
150 pub fn new() -> Self {
155 Self {
156 size: UVec2::new(32, 32),
157 pixel_size: UVec2::ONE,
158 }
159 }
160
161 pub fn size(size: impl Into<UVec2>) -> Self {
163 Self {
164 size: size.into(),
165 ..Default::default()
166 }
167 }
168
169 pub fn pixel_size(pixel_size: impl Into<UVec2>) -> Self {
173 Self {
174 pixel_size: pixel_size.into(),
175 ..Default::default()
176 }
177 }
178
179 pub fn screen_size(&self) -> UVec2 {
181 self.size * self.pixel_size
182 }
183}
184
185impl Default for PixelBufferSize {
186 fn default() -> Self {
187 Self::new()
188 }
189}
190
191pub struct CreateImageParams {
193 pub size: UVec2,
195 pub label: Option<&'static str>,
197 pub usage: TextureUsages,
204 pub sampler_descriptor: ImageSampler,
208}
209
210impl Default for CreateImageParams {
211 fn default() -> Self {
212 Self {
213 size: UVec2 { x: 32, y: 32 },
214 label: None,
215 usage: TextureUsages::TEXTURE_BINDING
216 | TextureUsages::COPY_DST
217 | TextureUsages::STORAGE_BINDING,
218 sampler_descriptor: ImageSampler::nearest(),
219 }
220 }
221}
222
223impl From<UVec2> for CreateImageParams {
224 fn from(size: UVec2) -> Self {
225 Self {
226 size,
227 ..Default::default()
228 }
229 }
230}
231
232pub fn create_image(params: CreateImageParams) -> Image {
245 let CreateImageParams {
246 size,
247 label,
248 usage,
249 sampler_descriptor,
250 } = params;
251
252 assert_ne!(size.x, 0);
253 assert_ne!(size.y, 0);
254 assert!(usage.contains(
255 TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::STORAGE_BINDING
256 ));
257
258 let mut image = Image {
259 texture_descriptor: TextureDescriptor {
260 label,
261 size: Extent3d {
262 width: size.x,
263 height: size.y,
264 depth_or_array_layers: 1,
265 },
266 mip_level_count: 1,
267 sample_count: 1,
268 dimension: TextureDimension::D2,
269 format: Pixel::FORMAT,
270 usage,
271 view_formats: &[],
272 },
273 data: vec![],
274 sampler: sampler_descriptor,
275 texture_view_descriptor: None,
276 asset_usage: Default::default(),
277 };
278 image.resize(image.texture_descriptor.size); image
280}
281
282pub struct PixelBufferPlugins;
287
288impl PluginGroup for PixelBufferPlugins {
289 #[allow(clippy::let_and_return)]
290 fn build(self) -> PluginGroupBuilder {
291 let group = PluginGroupBuilder::start::<Self>();
292
293 let group = group.add(PixelBufferPlugin);
294 #[cfg(feature = "egui")]
295 let group = group.add(crate::egui::PixelBufferEguiPlugin);
296
297 group
298 }
299}
300
301pub struct PixelBufferPlugin;
303
304impl Plugin for PixelBufferPlugin {
305 fn build(&self, app: &mut App) {
306 app.add_systems(PreUpdate, fill)
307 .add_systems(PreUpdate, (resize, sprite_custom_size).after(fill));
308 }
309}
310
311#[allow(clippy::type_complexity)]
313fn resize(
314 pixel_buffer: Query<
315 (&Handle<Image>, &PixelBuffer),
316 Or<(Changed<PixelBuffer>, Added<Handle<Image>>)>,
317 >,
318 mut images: ResMut<Assets<Image>>,
319) {
320 for (image_handle, pb) in pixel_buffer.iter() {
321 let PixelBuffer { size, .. } = pb;
322
323 if size.size.x == 0 || size.size.y == 0 || size.pixel_size.x == 0 || size.pixel_size.y == 0
324 {
325 warn!("Skipping resize, with and/or height are 0");
326 return;
327 }
328
329 let image = images.get(image_handle).expect("pixel buffer image");
330
331 if size.size != image.size() {
332 let image = images.get_mut(image_handle).expect("pixel buffer image");
333
334 info!("Resizing image to: {:?}", size);
335 image.resize(Extent3d {
336 width: size.size.x,
337 height: size.size.y,
338 depth_or_array_layers: 1,
339 });
340 }
341 }
342}
343
344fn fill(
346 mut pixel_buffer: Query<&mut PixelBuffer>,
347 primary_window: Query<&Window, With<PrimaryWindow>>,
348) {
349 for mut pb in pixel_buffer.iter_mut() {
350 if let Some(fill_area) = get_fill_area(&pb, primary_window.get_single().ok()) {
351 let PixelBuffer { size, fill } = pb.as_ref();
352
353 let new_buffer_size = fill_area.as_uvec2() / size.pixel_size;
354 let new_buffer_size = (new_buffer_size / fill.multiple) * fill.multiple;
356
357 if new_buffer_size != size.size {
358 pb.size.size = new_buffer_size;
359 }
360 }
361 }
362}
363
364#[allow(clippy::type_complexity)]
366fn sprite_custom_size(
367 mut pixel_buffer: Query<(&PixelBuffer, &mut Sprite)>,
368 primary_window: Query<&Window, With<PrimaryWindow>>,
369) {
370 for (pb, mut sprite) in pixel_buffer.iter_mut() {
371 let mut new_size = pb.size.screen_size().as_vec2();
372
373 if pb.fill.stretch {
375 if let Some(fill_area) = get_fill_area(pb, primary_window.get_single().ok()) {
377 new_size = fill_area;
378 }
379 }
380
381 let new_size = Some(new_size);
382 if new_size != sprite.as_ref().custom_size {
384 info!("Resizing sprite to: {:?}", new_size);
385 sprite.custom_size = new_size;
386 }
387 }
388}
389
390pub(crate) fn get_fill_area(pb: &PixelBuffer, window: Option<&Window>) -> Option<Vec2> {
391 match pb.fill.kind {
392 FillKind::None => None,
393 FillKind::Window => window.map(|window| Vec2::new(window.width(), window.height())),
394 FillKind::Custom(custom_size) => Some(custom_size),
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401 use crate::bundle::{PixelBufferBundle, PixelBufferSpriteBundle};
402
403 #[test]
404 fn do_resize_image() {
405 let mut app = App::new();
406
407 app.add_plugins(MinimalPlugins)
408 .add_plugins(bevy::asset::AssetPlugin::default())
409 .add_plugins(bevy::render::texture::ImagePlugin::default());
410
411 app.add_systems(Update, resize);
412
413 let initial_size = UVec2::new(5, 5);
414 let set_size = UVec2::new(10, 10);
415
416 assert_ne!(initial_size, set_size);
417
418 let mut images = app.world_mut().resource_mut::<Assets<Image>>();
419 let image = images.add(create_image(initial_size.into()));
420
421 let pb_id = app
422 .world_mut()
423 .spawn(PixelBufferBundle {
424 pixel_buffer: PixelBuffer {
425 size: PixelBufferSize::size(set_size),
426 fill: Fill::none(),
427 },
428 image,
429 })
430 .id();
431
432 app.update();
433
434 let set_size = app.world().get::<PixelBuffer>(pb_id).unwrap().size.size;
435 let image_handle = app.world().get::<Handle<Image>>(pb_id).unwrap();
436 let images = app.world().resource::<Assets<Image>>();
437 let image_size = images.get(image_handle).unwrap().size();
438
439 assert_eq!(set_size, image_size);
440 }
441
442 #[test]
443 fn do_resize_sprite() {
444 let mut app = App::new();
445
446 app.add_plugins(MinimalPlugins)
447 .add_plugins(bevy::asset::AssetPlugin::default())
448 .add_plugins(bevy::render::texture::ImagePlugin::default());
449
450 app.add_systems(Update, sprite_custom_size);
451
452 let set_size = UVec2::new(10, 10);
453
454 let mut images = app.world_mut().resource_mut::<Assets<Image>>();
455 let image = images.add(create_image(set_size.into()));
456
457 let pb_id = app
458 .world_mut()
459 .spawn(PixelBufferSpriteBundle {
460 pixel_buffer: PixelBuffer {
461 size: PixelBufferSize::size(set_size),
462 fill: Fill::none(),
463 },
464 sprite_bundle: SpriteBundle {
465 sprite: Sprite {
466 custom_size: None,
467 ..Default::default()
468 },
469 texture: image,
470 ..Default::default()
471 },
472 })
473 .id();
474
475 app.update();
476
477 let size = app.world().get::<PixelBuffer>(pb_id).unwrap().size;
478 let sprite = app.world().get::<Sprite>(pb_id).unwrap();
479
480 assert!(sprite.custom_size.is_some());
481 assert_eq!(size.screen_size(), sprite.custom_size.unwrap().as_uvec2());
482 }
483
484 #[test]
485 fn do_fill() {
486 let mut app = App::new();
487
488 app.add_plugins(MinimalPlugins)
489 .add_plugins(bevy::asset::AssetPlugin::default())
490 .add_plugins(bevy::render::texture::ImagePlugin::default());
491
492 app.add_systems(Update, fill);
493
494 let set_size = UVec2::new(5, 5);
495 let fill_area = Vec2::new(10.5, 10.4);
496
497 let mut images = app.world_mut().resource_mut::<Assets<Image>>();
498 let image = images.add(create_image(set_size.into()));
499
500 let pb_id = app
501 .world_mut()
502 .spawn(PixelBufferBundle {
503 pixel_buffer: PixelBuffer {
504 size: PixelBufferSize::size(set_size),
505 fill: Fill::custom(fill_area),
506 },
507 image,
508 })
509 .id();
510
511 app.update();
512
513 let size = app.world().get::<PixelBuffer>(pb_id).unwrap().size.size;
514 assert_eq!(size, UVec2::new(10, 10));
515 }
516}