Skip to main content

cranpose_render_pixels/
sprites.rs

1//! The scene as sprites for a GPU compositor other than wgpu: SpriteKit on
2//! watchOS, which gives an app no Metal of its own.
3//!
4//! Every item becomes one quad in device pixels, drawn in order, cut to the
5//! whole pixels its clip leaves, as the rasterizer cuts it. The shapes the
6//! compositor's shader covers -- filled and stroked rounded rects, arcs and
7//! lines, painted with a solid colour or a two-colour gradient -- travel as
8//! numbers. Everything else is drawn here, once, by the rasterizer into a
9//! texture of its own, which is kept while the item stays the same: moving it
10//! by whole pixels moves the quad and keeps the texture.
11//!
12//! A sprite is [`SPRITE_FLOATS`] numbers; [`field`] names where each part
13//! starts. Positions are device pixels with the origin at the top left.
14
15use std::{fmt::Write as _, hash::Hasher};
16
17use cranpose_render_common::{
18    brush_sampling::color_to_rgba,
19    graph_scene::RenderDiagnostics,
20    layer_shadow::{ShadowHole, ShadowRRect},
21};
22use cranpose_ui_graphics::{
23    BlendMode, Brush, Color, FxHashMap, FxHasher, Point, Rect, StrokeCap, StrokeJoin, TileMode,
24    whole,
25};
26
27use crate::{
28    draw::{
29        ClipBounds, RenderItem, ShapeCoverage, Target, TextRaster, clip_rect_to_window, draw_item,
30        place_shape, snap_delta_for_anchor,
31    },
32    scene::{DrawShape, RasterScene, ShadowDraw, TextDraw},
33};
34
35/// How many numbers make one sprite.
36pub const SPRITE_FLOATS: usize = 36;
37
38/// Where each part of a sprite starts, and what it holds.
39pub mod field {
40    /// The quad: x, y, width, height.
41    pub const FRAME: usize = 0;
42    /// What the quad holds ([`super::SpriteKind`]), the texture it shows
43    /// (0 for none), how it blends (0 source over, 1 destination out) and its
44    /// brush (0 solid, 1 linear, 2 radial, 3 sweep).
45    pub const KIND: usize = 4;
46    /// The geometry, by kind. A texture: the rect the whole texture covers.
47    /// A fill or a stroke: the rect. An arc: centre x and y, inner and outer
48    /// radius. A line: start x and y, end x and y.
49    pub const GEOMETRY: usize = 8;
50    /// A fill or a stroke: the corner radii, top-left, top-right,
51    /// bottom-right, bottom-left. An arc: start angle, sweep, cap (0 butt,
52    /// 1 round, 2 square). A line: half width, cap.
53    pub const DETAIL: usize = 12;
54    /// A fill: whether its corners are rounded (1) or not (0). A stroke: half
55    /// width and join (0 miter, 1 round, 2 bevel). Third: a gradient's tile
56    /// mode (0 clamp, 1 repeated, 2 mirror, 3 decal).
57    pub const STROKE: usize = 16;
58    /// The brush's first colour, straight RGBA.
59    pub const COLOR: usize = 20;
60    /// A gradient's second colour, straight RGBA.
61    pub const COLOR_END: usize = 24;
62    /// A gradient's geometry: linear start x, y and end x, y; radial centre
63    /// x, y and radius; sweep centre x, y.
64    pub const GRADIENT: usize = 28;
65    /// A gradient's two stops, then the origin its dither is measured from.
66    pub const STOPS: usize = 32;
67}
68
69/// What one sprite holds.
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71#[repr(u8)]
72pub enum SpriteKind {
73    /// A texture that the rasterizer drew for the item.
74    Texture = 0,
75    /// A filled rounded rectangle.
76    Fill = 1,
77    /// A stroked rounded rectangle.
78    Stroke = 2,
79    /// A circular arc band.
80    Arc = 3,
81    /// A stroked line segment.
82    Line = 4,
83}
84
85/// A texture the compositor has not seen yet: premultiplied RGBA8, row by
86/// row from the top.
87#[derive(Clone, Debug, PartialEq, Eq)]
88pub struct TextureUpload {
89    /// Handle of the texture. Sprites name it, and `releases` lists it once it is dropped.
90    pub id: u32,
91    /// Texture width in pixels.
92    pub width: u32,
93    /// Texture height in pixels.
94    pub height: u32,
95    /// Premultiplied RGBA8 pixels, row by row from the top: `width` times `height` times four
96    /// bytes.
97    pub pixels: Vec<u8>,
98}
99
100/// One frame of sprites, and the textures they use. It keeps its textures
101/// from frame to frame: compose into the same one every frame.
102#[derive(Default)]
103pub struct SpriteFrame {
104    /// The scene the last frame composed, whose storage the next one reuses.
105    pub(crate) scene: RasterScene,
106    sprites: Vec<f32>,
107    uploads: Vec<TextureUpload>,
108    releases: Vec<u32>,
109    textures: FxHashMap<u64, CachedTexture>,
110    next_texture: u32,
111    generation: u64,
112}
113
114struct CachedTexture {
115    id: u32,
116    /// Where the texture sits for the item at its canonical place.
117    placement: Rect,
118    last_used: u64,
119}
120
121/// Frames a texture outlives the last item that showed it, so an item that
122/// blinks does not draw itself again each time it comes back.
123const TEXTURE_RETENTION_FRAMES: u64 = 60;
124
125impl SpriteFrame {
126    /// The sprites, [`SPRITE_FLOATS`] numbers each, in drawing order.
127    pub fn sprites(&self) -> &[f32] {
128        &self.sprites
129    }
130
131    /// Number of sprites in the frame.
132    pub fn sprite_count(&self) -> usize {
133        self.sprites.len() / SPRITE_FLOATS
134    }
135
136    /// Textures first used this frame.
137    pub fn uploads(&self) -> &[TextureUpload] {
138        &self.uploads
139    }
140
141    /// Textures no sprite has used for a while; the compositor drops them.
142    pub fn releases(&self) -> &[u32] {
143        &self.releases
144    }
145
146    pub(crate) fn compose(
147        &mut self,
148        scene: &RasterScene,
149        width: u32,
150        height: u32,
151        diagnostics: &RenderDiagnostics,
152        text: &TextRaster,
153    ) {
154        self.sprites.clear();
155        self.uploads.clear();
156        self.releases.clear();
157        self.generation += 1;
158        let screen = ClipBounds {
159            min_x: 0,
160            min_y: 0,
161            max_x: width as i32,
162            max_y: height as i32,
163        };
164        for &item in &scene.order {
165            if let RenderItem::Shape(index) = item
166                && let Some(sprite) = shape_sprite(&scene.shapes[index], screen)
167            {
168                self.sprites.extend_from_slice(&sprite);
169                continue;
170            }
171            self.texture_sprite(scene, item, screen, diagnostics, text);
172        }
173        let generation = self.generation;
174        let releases = &mut self.releases;
175        self.textures.retain(|_, texture| {
176            let keep = generation - texture.last_used <= TEXTURE_RETENTION_FRAMES;
177            if !keep {
178                releases.push(texture.id);
179            }
180            keep
181        });
182    }
183
184    /// Shows `item` from a texture the rasterizer drew it into, drawing it
185    /// first if no texture holds it at its canonical place.
186    fn texture_sprite(
187        &mut self,
188        scene: &RasterScene,
189        item: RenderItem,
190        screen: ClipBounds,
191        diagnostics: &RenderDiagnostics,
192        text: &TextRaster,
193    ) {
194        let Some((canonical, offset, clip)) = Canonical::of(scene, item) else {
195            return;
196        };
197        let key = canonical.key();
198        let generation = self.generation;
199        let (id, placement) = match self.textures.get_mut(&key) {
200            Some(texture) => {
201                texture.last_used = generation;
202                (texture.id, texture.placement)
203            }
204            None => {
205                let Some((placement, pixels)) = canonical.rasterize(diagnostics, text) else {
206                    return;
207                };
208                self.next_texture = self.next_texture.wrapping_add(1).max(1);
209                let id = self.next_texture;
210                self.uploads.push(TextureUpload {
211                    id,
212                    width: placement.width as u32,
213                    height: placement.height as u32,
214                    pixels,
215                });
216                self.textures.insert(
217                    key,
218                    CachedTexture {
219                        id,
220                        placement,
221                        last_used: generation,
222                    },
223                );
224                (id, placement)
225            }
226        };
227        let placement = placement.translate(offset.x, offset.y);
228        let Some(frame) = clip_rect_to_window(placement, clip, screen) else {
229            return;
230        };
231        let mut sprite = [0.0; SPRITE_FLOATS];
232        put_frame(&mut sprite, frame);
233        sprite[field::KIND] = SpriteKind::Texture as u8 as f32;
234        sprite[field::KIND + 1] = id as f32;
235        put_rect(&mut sprite, field::GEOMETRY, placement);
236        self.sprites.extend_from_slice(&sprite);
237    }
238}
239
240fn put_frame(sprite: &mut [f32; SPRITE_FLOATS], frame: ClipBounds) {
241    sprite[field::FRAME] = frame.min_x as f32;
242    sprite[field::FRAME + 1] = frame.min_y as f32;
243    sprite[field::FRAME + 2] = (frame.max_x - frame.min_x) as f32;
244    sprite[field::FRAME + 3] = (frame.max_y - frame.min_y) as f32;
245}
246
247fn put_rect(sprite: &mut [f32; SPRITE_FLOATS], at: usize, rect: Rect) {
248    sprite[at..at + 4].copy_from_slice(&[rect.x, rect.y, rect.width, rect.height]);
249}
250
251fn put_color(sprite: &mut [f32; SPRITE_FLOATS], at: usize, color: Color) {
252    sprite[at..at + 4].copy_from_slice(&color_to_rgba(color));
253}
254
255fn cap_code(cap: StrokeCap) -> f32 {
256    match cap {
257        StrokeCap::Butt => 0.0,
258        StrokeCap::Round => 1.0,
259        StrokeCap::Square => 2.0,
260    }
261}
262
263fn join_code(join: StrokeJoin) -> f32 {
264    match join {
265        StrokeJoin::Miter => 0.0,
266        StrokeJoin::Round => 1.0,
267        StrokeJoin::Bevel => 2.0,
268    }
269}
270
271fn tile_code(tile_mode: TileMode) -> f32 {
272    match tile_mode {
273        TileMode::Clamp => 0.0,
274        TileMode::Repeated => 1.0,
275        TileMode::Mirror => 2.0,
276        TileMode::Decal => 3.0,
277    }
278}
279
280/// The sprite the compositor's shader draws `draw` with, or `None` when the
281/// shader does not cover it and a texture must.
282fn shape_sprite(draw: &DrawShape, screen: ClipBounds) -> Option<[f32; SPRITE_FLOATS]> {
283    let blend = match draw.blend_mode {
284        BlendMode::DstOut => 1.0,
285        _ => 0.0,
286    };
287    let placed = place_shape(draw)?;
288    let mut sprite = [0.0; SPRITE_FLOATS];
289    match placed.coverage {
290        ShapeCoverage::Fill { rect, radii } => {
291            sprite[field::KIND] = SpriteKind::Fill as u8 as f32;
292            put_rect(&mut sprite, field::GEOMETRY, rect);
293            if let Some(radii) = radii {
294                sprite[field::DETAIL..field::DETAIL + 4].copy_from_slice(&[
295                    radii.top_left,
296                    radii.top_right,
297                    radii.bottom_right,
298                    radii.bottom_left,
299                ]);
300                sprite[field::STROKE] = 1.0;
301            }
302        }
303        ShapeCoverage::Stroke {
304            rect,
305            radii,
306            half_width,
307            join,
308        } => {
309            sprite[field::KIND] = SpriteKind::Stroke as u8 as f32;
310            put_rect(&mut sprite, field::GEOMETRY, rect);
311            let radii = radii.unwrap_or_default();
312            sprite[field::DETAIL..field::DETAIL + 4].copy_from_slice(&[
313                radii.top_left,
314                radii.top_right,
315                radii.bottom_right,
316                radii.bottom_left,
317            ]);
318            sprite[field::STROKE] = half_width;
319            sprite[field::STROKE + 1] = join_code(join);
320        }
321        ShapeCoverage::Arc(arc) => {
322            sprite[field::KIND] = SpriteKind::Arc as u8 as f32;
323            sprite[field::GEOMETRY..field::GEOMETRY + 4].copy_from_slice(&[
324                arc.center.x,
325                arc.center.y,
326                arc.inner_radius,
327                arc.outer_radius,
328            ]);
329            sprite[field::DETAIL..field::DETAIL + 3].copy_from_slice(&[
330                arc.start_angle,
331                arc.sweep_angle,
332                cap_code(arc.cap),
333            ]);
334        }
335        ShapeCoverage::Line(line) => {
336            sprite[field::KIND] = SpriteKind::Line as u8 as f32;
337            sprite[field::GEOMETRY..field::GEOMETRY + 4].copy_from_slice(&[
338                line.start.x,
339                line.start.y,
340                line.end.x,
341                line.end.y,
342            ]);
343            sprite[field::DETAIL] = line.half_width;
344            sprite[field::DETAIL + 1] = cap_code(line.cap);
345        }
346        ShapeCoverage::Trapezoid(_) => return None,
347    }
348    put_brush(&mut sprite, &draw.brush, placed.rect)?;
349    sprite[field::KIND + 2] = blend;
350    let frame = clip_rect_to_window(placed.coverage.reach(placed.rect), draw.clip, screen)?;
351    put_frame(&mut sprite, frame);
352    // The dither's origin, which the shader measures pixels from.
353    sprite[field::STOPS + 2] = placed.dither_origin.x;
354    sprite[field::STOPS + 3] = placed.dither_origin.y;
355    Some(sprite)
356}
357
358/// Writes the brush the shader paints with, resolved against `rect` as the
359/// rasterizer resolves it; `None` for a gradient of more than two colours.
360fn put_brush(sprite: &mut [f32; SPRITE_FLOATS], brush: &Brush, rect: Rect) -> Option<()> {
361    let two_colours = |colors: &[Color], stops: Option<&[f32]>| match (colors, stops) {
362        ([only], _) => Some((*only, *only, [0.0, 1.0])),
363        ([start, end], Some([from, to])) => Some((*start, *end, [*from, *to])),
364        ([start, end], _) => Some((*start, *end, [0.0, 1.0])),
365        _ => None,
366    };
367    let resolve = |origin: f32, extent: f32, value: f32| {
368        if value.is_finite() {
369            origin + value
370        } else if value.is_sign_positive() {
371            origin + extent
372        } else {
373            origin
374        }
375    };
376    let (code, (start, end, stops), gradient, tile) = match brush {
377        Brush::Solid(color) => (0.0, (*color, *color, [0.0, 1.0]), [0.0; 4], TileMode::Clamp),
378        Brush::LinearGradient {
379            colors,
380            stops,
381            start,
382            end,
383            tile_mode,
384        } => (
385            1.0,
386            two_colours(colors, stops.as_deref())?,
387            [
388                resolve(rect.x, rect.width, start.x),
389                resolve(rect.y, rect.height, start.y),
390                resolve(rect.x, rect.width, end.x),
391                resolve(rect.y, rect.height, end.y),
392            ],
393            *tile_mode,
394        ),
395        Brush::RadialGradient {
396            colors,
397            stops,
398            center,
399            radius,
400            tile_mode,
401        } => (
402            2.0,
403            two_colours(colors, stops.as_deref())?,
404            [
405                rect.x + center.x,
406                rect.y + center.y,
407                radius.max(f32::EPSILON),
408                0.0,
409            ],
410            *tile_mode,
411        ),
412        Brush::SweepGradient {
413            colors,
414            stops,
415            center,
416        } => (
417            3.0,
418            two_colours(colors, stops.as_deref())?,
419            [rect.x + center.x, rect.y + center.y, 0.0, 0.0],
420            TileMode::Clamp,
421        ),
422    };
423    sprite[field::KIND + 3] = code;
424    put_color(sprite, field::COLOR, start);
425    put_color(sprite, field::COLOR_END, end);
426    sprite[field::GRADIENT..field::GRADIENT + 4].copy_from_slice(&gradient);
427    sprite[field::STOPS] = stops[0];
428    sprite[field::STOPS + 1] = stops[1];
429    sprite[field::STROKE + 2] = tile_code(tile);
430    Some(())
431}
432
433/// An item moved to its canonical place: shifted by whole pixels so its own
434/// rect starts within the first pixel, with no clip. Two frames that show the
435/// same item at whole-pixel distances share one canonical item.
436struct Canonical {
437    scene: RasterScene,
438    item: RenderItem,
439}
440
441impl Canonical {
442    /// The canonical item, the whole-pixel shift back to where `item` is,
443    /// and its clip.
444    fn of(scene: &RasterScene, item: RenderItem) -> Option<(Self, Point, Option<Rect>)> {
445        let mut canonical = RasterScene::new();
446        let (origin, clip) = match item {
447            RenderItem::Shape(index) => {
448                let draw = &scene.shapes[index];
449                (Point::new(draw.rect.x, draw.rect.y), draw.clip)
450            }
451            RenderItem::Image(index) => {
452                let draw = &scene.images[index];
453                (Point::new(draw.rect.x, draw.rect.y), draw.clip)
454            }
455            RenderItem::Text(index) => {
456                let draw = &scene.texts[index];
457                (Point::new(draw.rect.x, draw.rect.y), draw.clip)
458            }
459            RenderItem::Shadow(index) => {
460                let draw = &scene.shadows[index];
461                let snap = draw
462                    .snap_anchor
463                    .map(snap_delta_for_anchor)
464                    .unwrap_or_default();
465                (
466                    Point::new(draw.shadow.bounds.x, draw.shadow.bounds.y),
467                    draw.clip.map(|clip| clip.translate(snap.x, snap.y)),
468                )
469            }
470        };
471        if !(origin.x.is_finite() && origin.y.is_finite()) {
472            return None;
473        }
474        let shift = Point::new(whole::floor(origin.x), whole::floor(origin.y));
475        let item = match item {
476            RenderItem::Shape(index) => {
477                canonical
478                    .shapes
479                    .push(scene.shapes[index].translated(-shift.x, -shift.y));
480                RenderItem::Shape(0)
481            }
482            RenderItem::Image(index) => {
483                canonical
484                    .images
485                    .push(scene.images[index].translated(-shift.x, -shift.y));
486                RenderItem::Image(0)
487            }
488            RenderItem::Text(index) => {
489                canonical
490                    .texts
491                    .push(scene.texts[index].translated(-shift.x, -shift.y));
492                RenderItem::Text(0)
493            }
494            RenderItem::Shadow(index) => {
495                canonical
496                    .shadows
497                    .push(scene.shadows[index].translated(-shift.x, -shift.y));
498                RenderItem::Shadow(0)
499            }
500        };
501        Some((
502            Self {
503                scene: canonical,
504                item,
505            },
506            shift,
507            clip,
508        ))
509    }
510
511    /// A hash of everything that decides the canonical item's pixels.
512    fn key(&self) -> u64 {
513        let Self { scene, item } = self;
514        let mut hasher = HashWriter(FxHasher::default());
515        // A text's and a shadow's fields are hashed as numbers. A shape's and
516        // an image's `Debug` forms name every field, positions included; an
517        // image's pixels are named by the bitmap's id instead of printed.
518        let _ = match item {
519            RenderItem::Text(_) => {
520                hash_text(&mut hasher.0, &scene.texts[0]);
521                Ok(())
522            }
523            RenderItem::Shadow(_) => {
524                hash_shadow(&mut hasher.0, &scene.shadows[0]);
525                Ok(())
526            }
527            RenderItem::Shape(_) => write!(hasher, "shape {:?}", scene.shapes[0]),
528            RenderItem::Image(_) => {
529                let draw = &scene.images[0];
530                write!(
531                    hasher,
532                    "image {} {:?} {:?} {} {:?} {:?} {:?} {:?} {:?}",
533                    draw.image.id(),
534                    draw.rect,
535                    draw.snap_anchor,
536                    draw.alpha,
537                    draw.color_filter,
538                    draw.sampling,
539                    draw.blend_mode,
540                    draw.src_rect,
541                    draw.transform,
542                )
543            }
544        };
545        hasher.0.finish()
546    }
547
548    /// Draws the canonical item into a texture of the whole pixels it
549    /// reaches: where the texture sits and its premultiplied pixels.
550    fn rasterize(
551        &self,
552        diagnostics: &RenderDiagnostics,
553        text: &TextRaster,
554    ) -> Option<(Rect, Vec<u8>)> {
555        let Self { scene, item } = self;
556        if let RenderItem::Text(_) = item {
557            return rasterize_text(&scene.texts[0], text);
558        }
559        let reach = scene.item_reach(*item)?;
560        let window = clip_rect_to_window(
561            reach,
562            None,
563            ClipBounds {
564                min_x: i32::MIN / 4,
565                min_y: i32::MIN / 4,
566                max_x: i32::MAX / 4,
567                max_y: i32::MAX / 4,
568            },
569        )?;
570        let width = (window.max_x - window.min_x) as u32;
571        let height = (window.max_y - window.min_y) as u32;
572        let mut pixels = vec![0u8; width as usize * height as usize * 4];
573        let mut target = Target {
574            pixels: pixels.as_chunks_mut::<4>().0,
575            left: window.min_x,
576            top: window.min_y,
577            width,
578            height,
579        };
580        draw_item(&mut target, scene, *item, diagnostics, text);
581        Some((
582            Rect {
583                x: window.min_x as f32,
584                y: window.min_y as f32,
585                width: width as f32,
586                height: height as f32,
587            },
588            pixels,
589        ))
590    }
591}
592
593/// Text drawn as the rasterizer draws it, into its own premultiplied image.
594fn rasterize_text(draw: &TextDraw, text: &TextRaster) -> Option<(Rect, Vec<u8>)> {
595    let (text, rect) = crate::draw::rasterize_text(draw, text)?;
596    let mut pixels = text.pixels;
597    // `(c * a + 127) / 255` for each channel, in 16-bit lanes with no
598    // division and no branch, so that a vector unit takes several pixels at
599    // once: for every `x` up to 65,152, `x / 255` is
600    // `(x + 1 + (x >> 8)) >> 8`.
601    for pixel in pixels.as_chunks_mut::<4>().0 {
602        let alpha = u16::from(pixel[3]);
603        for channel in &mut pixel[..3] {
604            let x = u16::from(*channel) * alpha + 127;
605            *channel = ((x + 1 + (x >> 8)) >> 8) as u8;
606        }
607    }
608    Some((rect, pixels))
609}
610
611// A text's draw: its numbers, and the render hashes of its string and style,
612// which hash every setting that changes how the text looks, as the
613// renderers' text caches key on them. Formatting the draw for its `Debug`
614// form took about half of a watch frame's instructions. Every field is named,
615// so a new one does not build until it is hashed here too.
616fn hash_text(hasher: &mut FxHasher, draw: &TextDraw) {
617    let TextDraw {
618        rect,
619        snap_anchor,
620        text,
621        layout,
622        ink,
623        color,
624        text_style,
625        font_size,
626        scale,
627        clip,
628    } = draw;
629    hasher.write_u8(b't');
630    hash_rect(hasher, *rect);
631    hash_floats(hasher, &[layout.width(), layout.height()]);
632    hasher.write_u8(u8::from(
633        *ink == cranpose_render_common::text_raster::GlyphInk::Hidden,
634    ));
635    hash_point(hasher, *snap_anchor);
636    hasher.write_u64(text.render_hash());
637    hash_color(hasher, *color);
638    hasher.write_u64(text_style.render_hash());
639    hash_floats(hasher, &[*font_size, *scale]);
640    hash_clip(hasher, *clip);
641}
642
643// A shadow's draw, every field of it as numbers.
644fn hash_shadow(hasher: &mut FxHasher, draw: &ShadowDraw) {
645    let ShadowDraw {
646        shadow:
647            ShadowRRect {
648                bounds,
649                radii,
650                umbra_inset,
651                distance_correction,
652                hole,
653            },
654        snap_anchor,
655        color,
656        clip,
657    } = draw;
658    hasher.write_u8(b's');
659    hash_rect(hasher, *bounds);
660    hash_floats(hasher, radii);
661    hash_floats(hasher, &[*umbra_inset, *distance_correction]);
662    match hole {
663        Some(ShadowHole { inset, radius }) => {
664            hasher.write_u8(1);
665            hash_floats(hasher, &[*inset, *radius]);
666        }
667        None => hasher.write_u8(0),
668    }
669    hash_point(hasher, *snap_anchor);
670    hash_color(hasher, *color);
671    hash_clip(hasher, *clip);
672}
673
674fn hash_floats(hasher: &mut FxHasher, values: &[f32]) {
675    for value in values {
676        hasher.write_u32(value.to_bits());
677    }
678}
679
680fn hash_color(hasher: &mut FxHasher, color: Color) {
681    hash_floats(hasher, &[color.0, color.1, color.2, color.3]);
682}
683
684fn hash_clip(hasher: &mut FxHasher, clip: Option<Rect>) {
685    match clip {
686        Some(clip) => {
687            hasher.write_u8(1);
688            hash_rect(hasher, clip);
689        }
690        None => hasher.write_u8(0),
691    }
692}
693
694fn hash_rect(hasher: &mut FxHasher, rect: Rect) {
695    hash_floats(hasher, &[rect.x, rect.y, rect.width, rect.height]);
696}
697
698fn hash_point(hasher: &mut FxHasher, point: Option<Point>) {
699    match point {
700        Some(point) => {
701            hasher.write_u8(1);
702            hash_floats(hasher, &[point.x, point.y]);
703        }
704        None => hasher.write_u8(0),
705    }
706}
707
708struct HashWriter(FxHasher);
709
710impl std::fmt::Write for HashWriter {
711    fn write_str(&mut self, text: &str) -> std::fmt::Result {
712        self.0.write(text.as_bytes());
713        Ok(())
714    }
715}
716
717#[cfg(test)]
718#[path = "tests/sprites_tests.rs"]
719mod tests;