bevy_pixel_buffer/
pixel_buffer.rs

1//! Core systems and components of the pixel buffer library
2
3use 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/// Component defining a pixel buffer.
16///
17/// An [image handle](Handle<Image>) component is also
18/// needed for most operations, but can be added later.
19#[derive(Component, Default, Clone, Copy, Debug, PartialEq)]
20pub struct PixelBuffer {
21    /// Size of the pixel buffer
22    pub size: PixelBufferSize,
23    /// Fill mode
24    pub fill: Fill,
25}
26
27/// Size of a pixel buffer.
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub struct PixelBufferSize {
30    /// Number of (editable) pixels in each dimension.
31    pub size: UVec2,
32    /// Number of physical pixels each editable pixel takes up in the screen.
33    pub pixel_size: UVec2,
34}
35
36/// Fill behaviour of the pixel buffer, resizing it automatically
37#[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/// What to fill
45#[derive(Debug, Clone, Copy, PartialEq)]
46pub enum FillKind {
47    /// Fill disabled
48    None,
49    /// Fill a window
50    Window,
51    /// Fill a customs size
52    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    /// Fill disabled
67    pub fn none() -> Self {
68        Self {
69            kind: FillKind::None,
70            ..Default::default()
71        }
72    }
73
74    /// Fill disabled but stretch enabled for the future.
75    ///
76    /// This can be useful to set up an app with `egui` because
77    /// [Fill::update_egui] enables the filling later automatically.
78    ///
79    /// At the end it's an alias `Fill::none().with_stretch(true)`.
80    /// ```
81    /// # use bevy_pixel_buffer::pixel_buffer::Fill;
82    /// assert_eq!(Fill::stretch(), Fill::none().with_stretch(true));
83    /// ```
84    pub fn stretch() -> Self {
85        Self {
86            stretch: true,
87            ..Default::default()
88        }
89    }
90
91    /// Fill the primary window
92    pub fn window() -> Self {
93        Self {
94            kind: FillKind::Window,
95            ..Default::default()
96        }
97    }
98
99    /// Fill a custom size
100    pub fn custom(s: impl Into<Vec2>) -> Self {
101        Self {
102            kind: FillKind::Custom(s.into()),
103            ..Default::default()
104        }
105    }
106
107    /// Wether to stretch the rendering sprite to fill the area
108    pub fn with_stretch(mut self, stretch: bool) -> Self {
109        self.stretch = stretch;
110        self
111    }
112
113    /// Keep the size of the buffer a multiple of a value.
114    ///
115    /// Usefull for [ComputeShader](crate::compute_shader::ComputeShader)
116    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    /// New default size.
151    ///
152    /// - size: `32x32`
153    /// - pixel_size: `1x1`
154    pub fn new() -> Self {
155        Self {
156            size: UVec2::new(32, 32),
157            pixel_size: UVec2::ONE,
158        }
159    }
160
161    /// New with a custom size but default pixel_size
162    pub fn size(size: impl Into<UVec2>) -> Self {
163        Self {
164            size: size.into(),
165            ..Default::default()
166        }
167    }
168
169    /// New with a custom pixel_size but default size.
170    ///
171    /// Usefull combined with [Fill] as the size will be dynamically changed.
172    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    /// Returns how many physical pixels are necessary to draw the buffer.
180    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
191/// Parameters for [create_image].
192pub struct CreateImageParams {
193    /// Size of the image
194    pub size: UVec2,
195    /// wgpu label
196    pub label: Option<&'static str>,
197    /// Texture usages
198    ///
199    /// Has to include:
200    /// - [TextureUsages::TEXTURE_BINDING]
201    /// - [TextureUsages::COPY_DST]
202    /// - [TextureUsages::STORAGE_BINDING]
203    pub usage: TextureUsages,
204    /// Texture sampler
205    ///
206    /// For pixelated images the sensible sampler is [ImageSampler::nearest()].
207    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
232/// Creates a compatible [Image] with the pixel buffer.
233///
234/// The image needs to be added to the image assets to get a handle.
235///
236/// The image data is set to 0.
237///
238/// The wgpu format of the image is [Pixel::FORMAT].
239///
240/// # Panics
241/// - If the size is 0 in either dimension.
242/// - If the usages do not contain [TextureUsages::TEXTURE_BINDING],  [TextureUsages::COPY_DST] and [TextureUsages::STORAGE_BINDING].
243///
244pub 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); // set image data to 0
279    image
280}
281
282/// [Plugin group](PluginGroup) that adds the complete `bevy_pixel_buffer`
283/// suite of plugins:
284/// - [PixelBufferPlugin]
285/// - [PixelBufferEguiPlugin](crate::egui::PixelBufferEguiPlugin) *requires `egui` feature*
286pub 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
301/// [Plugin] that needs to be added to the app.
302pub 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/// Keeps the size in [PixelBuffer] in sync with the size of the underlying image.
312#[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
344/// Changes the size of the pixel buffer to match the fill
345fn 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            // Truncate to the fill multiple
355            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/// Changes the sprite custom size
365#[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 the sprite needs to stretch
374        if pb.fill.stretch {
375            // set its size to the fill area
376            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        // Make sure to not implicitly deref as mut
383        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}