Skip to main content

device_envoy_core/
cyd.rs

1#![cfg_attr(
2    feature = "doc-images",
3    doc = ::embed_doc_image::embed_image!(
4        "cyd_application_preview",
5        "docs/assets/cyd_application_preview.png"
6    )
7)]
8#![cfg_attr(
9    feature = "doc-images",
10    doc = ::embed_doc_image::embed_image!(
11        "linkage_blaze_gallery",
12        "docs/assets/linkage_blaze_gallery.png"
13    )
14)]
15//! Portable display and touch interfaces for Cheap Yellow Display (CYD)
16//! applications.
17//!
18//! The [`Cyd`] trait represents a ready-to-use device with a display and
19//! calibrated touch input. Hardware, browser, and in-memory devices all provide
20//! the same [`Cyd`], [`CydDisplay`], [`CydTouch`], and
21//! [`CydFrame`] interfaces.
22//!
23//! ## Portable CYD abstraction
24//!
25//! ```text
26//! CydEsp / CydRp / CydWasm / CydMemory
27//!                   │ implement
28//!                   ▼
29//!                  Cyd
30//!           ┌───────┴───────┐
31//!     parts().0         parts().1
32//!     CydDisplay        CydTouch
33//!          │                 │
34//!  frame_mut()          try_read()
35//!          ▼                 ▼
36//!     CydFrame          TouchEvent
37//!  borrowed frame       calibrated + oriented
38//! ```
39//!
40//! [`Cyd::parts`] borrows the display and touch components together.
41//! [`CydDisplay::frame_mut`] returns a temporary borrowed frame for a display
42//! region, while [`CydTouch::try_read`] returns already calibrated and oriented
43//! events. A full-screen frame is the special case where the borrowed region is
44//! the complete display.
45//!
46//! > **Touch-event coordinates and drawing coordinates use the same logical
47//! > orientation. Do not rotate touch points again.**
48#![cfg_attr(
49    not(feature = "doc-images"),
50    doc = "\n> **Incomplete documentation preview:** Gallery images are omitted because the `doc-images` feature is disabled. From the workspace root, use `just docs` for authoritative local documentation.\n"
51)]
52#![doc = include_str!("../docs/cyd/gallery.md")]
53#![doc = include_str!("../docs/cyd/application-example.md")]
54#![doc = include_str!("../docs/cyd/drawing-strategies.md")]
55#![doc = include_str!("../docs/cyd/implementations.md")]
56
57// This must remain public because the ESP and RP platform implementations live
58// in separate crates, but it is not part of the application-facing API.
59#[doc(hidden)]
60pub mod backend;
61pub mod display;
62pub mod touch;
63
64use display::{ContiguousPixels, CydFrame};
65
66/// Native panel width in pixels (landscape): 320. The CYD panel is fixed hardware.
67pub(crate) const SCREEN_WIDTH: usize = 320;
68/// Native panel height in pixels (landscape): 240. The CYD panel is fixed hardware.
69pub(crate) const SCREEN_HEIGHT: usize = 240;
70/// Total panel pixel count (`SCREEN_WIDTH * SCREEN_HEIGHT` = 320 * 240 = 76,800).
71///
72/// ```rust,no_run
73/// use device_envoy_core::cyd::SCREEN_PIXELS;
74/// // Platform static storage uses this exact size for a full-screen buffer.
75/// const PIXEL_BUFFER_SIZE: usize = SCREEN_PIXELS;
76/// assert_eq!(PIXEL_BUFFER_SIZE, 320 * 240);
77/// ```
78pub const SCREEN_PIXELS: usize = SCREEN_WIDTH * SCREEN_HEIGHT;
79
80use crate::pixel_target::rgb565_from_rgb888;
81use embedded_graphics::{
82    pixelcolor::{Rgb565, Rgb888},
83    prelude::{Point, Size},
84    primitives::Rectangle,
85};
86
87use display::Orientation;
88use touch::TouchEvent;
89
90/// A ready-to-use CYD device with display and calibrated touch components.
91///
92/// [`Cyd::parts`] borrows both components together. The associated types retain
93/// each implementation's concrete display, touch, and error types, while
94/// generic application code can accept any `C: Cyd`.
95///
96/// The [module-level example](index.html#application-example) shows how to use
97/// the display and calibrated touch input. To find the device's current
98/// orientation, see [`Cyd::orientation`]. See the
99/// [implementations](index.html#implementations-1) for `CydEsp`, `CydRp`,
100/// `CydWasm`, and `CydMemory`.
101pub trait Cyd: Sized {
102    /// Error returned by both the display and calibrated touch parts.
103    /// See the [application example](index.html#application-example) for a
104    /// generic operation that returns this error.
105    type Error;
106
107    type Display: CydDisplay<Error = Self::Error>;
108    type Touch: CydTouch<Error = Self::Error>;
109
110    /// Borrow the display and calibrated touch components at once.
111    ///
112    /// The [application example](index.html#application-example) uses `parts`
113    /// because its drawing loop needs both components simultaneously.
114    fn parts(&mut self) -> (&mut Self::Display, &mut Self::Touch);
115
116    /// Borrow the display component.
117    ///
118    /// See the [application example](index.html#application-example) for drawing
119    /// through a borrowed display component.
120    fn display(&mut self) -> &mut Self::Display {
121        self.parts().0
122    }
123
124    /// Borrow the calibrated touch component.
125    ///
126    /// See the [application example](index.html#application-example) for reading
127    /// calibrated, oriented touch events through a borrowed touch component.
128    fn touch(&mut self) -> &mut Self::Touch {
129        self.parts().1
130    }
131
132    /// Return the logical orientation of this complete device.
133    ///
134    /// # Example
135    ///
136    /// For a device constructed in landscape orientation, compare the returned
137    /// value directly and use it to obtain the application's logical display
138    /// dimensions. Portrait orientations instead return 240×320.
139    ///
140    /// ```rust,no_run
141    /// use device_envoy_core::cyd::{display::Orientation, Cyd};
142    /// use embedded_graphics::prelude::Size;
143    ///
144    /// fn check_landscape_orientation<C: Cyd>(device: &C) {
145    ///     let orientation = device.orientation();
146    ///     assert_eq!(orientation, Orientation::Landscape);
147    ///     assert_eq!(orientation.size(), Size::new(320, 240));
148    /// }
149    /// ```
150    fn orientation(&self) -> Orientation;
151}
152
153/// A CYD touch source that returns calibrated, oriented touch events in logical
154/// display coordinates.
155pub trait CydTouch: Sized {
156    /// Error returned when reading touch input.
157    type Error;
158
159    /// Try to read the next calibrated touch event without blocking.
160    ///
161    /// Returned points are calibrated and oriented into the same logical
162    /// coordinates as the display's [`CydDisplay::screen_size`]. `Ok(Some(event))`
163    /// means an event is available, `Ok(None)` means no event is available now,
164    /// and `Err(error)` means the underlying touch source could not be read.
165    ///
166    /// The example below consumes the already oriented point directly;
167    /// applications must not map it a second time.
168    /// The [application example](index.html#application-example) shows this
169    /// method in a complete read-and-draw flow.
170    ///
171    /// ```rust,no_run
172    /// use device_envoy_core::cyd::{CydTouch, touch::TouchEvent};
173    /// # use embedded_graphics::prelude::Point;
174    /// # fn handle_point(_point: Point) {}
175    ///
176    /// fn read_calibrated<T: CydTouch>(touch: &mut T) -> Result<(), T::Error> {
177    ///     if let Some(event) = touch.try_read()? {
178    ///         match event {
179    ///             TouchEvent::Down { point } | TouchEvent::Move { point } => {
180    ///                 // `point` is already in logical display coordinates.
181    ///                 handle_point(point);
182    ///             }
183    ///             TouchEvent::Up => {}
184    ///         }
185    ///     }
186    ///     Ok(())
187    /// }
188    /// ```
189    fn try_read(&mut self) -> Result<Option<TouchEvent>, Self::Error>;
190}
191
192/// A CYD display.
193///
194/// The screen is a fixed 320×240 RGB565 panel.
195///
196/// | Need | API | Reusable pixel-buffer storage |
197/// | --- | --- | ---: |
198/// | Normal drawing with enough RAM | [`full_frame_mut`](CydDisplay::full_frame_mut) | 153,600 bytes |
199/// | Redraw one region | [`frame_mut`](CydDisplay::frame_mut) | 2 × rectangle pixel count bytes |
200/// | Normal drawing with little RAM | [`for_each_tile`](CydDisplay::for_each_tile) | 2 × largest tile pixel count bytes |
201/// | Existing or generated row-major RGB565 pixels | [`fill_contiguous`](CydDisplay::fill_contiguous) or [`fill_contiguous_full`](CydDisplay::fill_contiguous_full) | No reusable frame buffer |
202/// | Small immediate [`DrawItem`](display::DrawItem) scene | [`draw_items`](CydDisplay::draw_items) | No pixel frame buffer |
203///
204/// The full-screen figure is `320 × 240 × 2` bytes for the fixed RGB565 panel.
205/// `draw_items` does not need a pixel frame buffer, but it does need
206/// allocation-free prepared-item capacity. Each nondegenerate `DrawItem`
207/// consumes at most one prepared-item slot, so setting the capacity to the
208/// number of supplied items is always safe. See [`CydDisplay::draw_items`] for
209/// details.
210///
211/// Start with [`CydDisplay::full_frame_mut`] when a 153,600-byte frame buffer is
212/// practical. The
213/// [drawing-strategy guide](index.html#choose-a-drawing-strategy) compares
214/// full-screen and regional buffering, tiled replay, and contiguous-pixel
215/// streaming.
216///
217pub trait CydDisplay: backend::DisplayBackend {
218    /// Screen size after applying the configured [`Orientation`]:
219    /// 320×240 in landscape or 240×320 in portrait.
220    ///
221    /// ```rust,no_run
222    /// use device_envoy_core::cyd::CydDisplay;
223    ///
224    /// # fn inspect(display: &impl CydDisplay) {
225    /// let size = display.screen_size();
226    /// assert!(
227    ///     (size.width == 320 && size.height == 240)
228    ///         || (size.width == 240 && size.height == 320)
229    /// );
230    /// # }
231    /// ```
232    fn screen_size(&self) -> Size;
233
234    /// The device default background color.
235    ///
236    /// ```rust,no_run
237    /// use device_envoy_core::cyd::CydDisplay;
238    ///
239    /// # fn inspect(display: &impl CydDisplay) {
240    /// let background = display.background_color();
241    /// let foreground = display.foreground_color();
242    /// assert_eq!(display.background_565(), display.to_rgb565(background));
243    /// assert_eq!(display.foreground_565(), display.to_rgb565(foreground));
244    /// # }
245    /// ```
246    fn background_color(&self) -> Rgb888;
247
248    /// The device default foreground/text color.
249    ///
250    /// See the [color getter example](CydDisplay::background_color).
251    fn foreground_color(&self) -> Rgb888;
252
253    /// The device default background color in the native `Rgb565` format.
254    ///
255    /// See the [color getter example](CydDisplay::background_color).
256    fn background_565(&self) -> Rgb565;
257
258    /// The device default foreground/text color in the native `Rgb565` format.
259    ///
260    /// See the [color getter example](CydDisplay::background_color).
261    fn foreground_565(&self) -> Rgb565;
262
263    /// Convert an `Rgb888` color to the device's native `Rgb565` format.
264    ///
265    /// See the [color getter example](CydDisplay::background_color).
266    fn to_rgb565(&self, color: Rgb888) -> Rgb565 {
267        rgb565_from_rgb888(color)
268    }
269
270    /// Borrow a frame covering `rectangle`, cleared to the device background color.
271    ///
272    /// See [`CydFrame`](display::CydFrame#coordinates-and-clipping) for the
273    /// shared screen-coordinate and clipping model.
274    ///
275    #[cfg_attr(
276        feature = "doc-images",
277        doc = ::embed_doc_image::embed_image!(
278            "cyd_frame_mut_preview",
279            "docs/assets/cyd_frame_mut_preview.png"
280        )
281    )]
282    #[cfg_attr(
283        feature = "host",
284        doc = r#"
285
286```rust
287use device_envoy_core::cyd::{CydDisplay, display::CydFrame};
288use embedded_graphics::{
289    pixelcolor::{Rgb565, RgbColor},
290    prelude::{Point, Size},
291    primitives::Rectangle,
292};
293
294async fn draw<D: CydDisplay>(display: &mut D) -> Result<(), D::Error> {
295    let mut frame = display.frame_mut(Rectangle::new(
296        Point::new(10, 10),
297        Size::new(100, 40),
298    ));
299    frame.fill(Rgb565::BLUE).write_text("CYD").flush().await
300}
301
302# use device_envoy_core::memory::{CydMemory, assert_framebuffer_matches_expected_png};
303# use embedded_graphics::{mono_font::ascii::FONT_9X15_BOLD, pixelcolor::Rgb888};
304# let memory_cyd = CydMemory::new(
305#     Size::new(320, 240),
306#     Rgb888::BLACK,
307#     Rgb888::WHITE,
308#     &FONT_9X15_BOLD,
309# );
310# let mut display = memory_cyd.display();
311# futures_executor::block_on(draw(&mut display))?;
312# if let Err(error) = assert_framebuffer_matches_expected_png(
313#     &memory_cyd,
314#     env!("CARGO_MANIFEST_DIR"),
315#     "cyd_frame_mut_preview.png",
316# ) {
317#     panic!("{error}");
318# }
319# Ok::<(), device_envoy_core::memory::Error>(())
320```
321
322"#
323    )]
324    #[cfg_attr(
325        all(feature = "host", feature = "doc-images"),
326        doc = "\n![CYD frame preview][cyd_frame_mut_preview]\n"
327    )]
328    fn frame_mut(&mut self, rectangle: Rectangle) -> Self::Frame<'_> {
329        backend::DisplayBackend::create_frame_mut(self, rectangle)
330    }
331
332    /// Borrow a full-screen frame, cleared to the device background color.
333    ///
334    /// See the [`Cyd` device-loop example](Cyd).
335    fn full_frame_mut(&mut self) -> Self::Frame<'_> {
336        self.frame_mut(Rectangle::new(Point::zero(), self.screen_size()))
337    }
338
339    /// Fill `rectangle` immediately with `color` in logical display coordinates.
340    ///
341    /// Unlike filling a frame returned by [`CydDisplay::frame_mut`], this is a
342    /// device-level operation rather than a frame-buffered draw. Implementations
343    /// clip to the logical display and treat an empty intersection as a no-op.
344    ///
345    /// The following example covers the immediate and contiguous operations:
346    /// [`CydDisplay::fill_contiguous`], [`CydDisplay::draw_items`], [`CydDisplay::clear`],
347    /// and [`CydDisplay::fill`].
348    ///
349    /// ```rust,no_run
350    /// use device_envoy_core::cyd::{CydDisplay, display::DrawItem};
351    /// use embedded_graphics::{pixelcolor::{Rgb565, Rgb888}, prelude::{Point, RgbColor, Size}, primitives::Rectangle};
352    ///
353    /// async fn draw<D: CydDisplay>(display: &mut D) -> Result<(), D::Error> {
354    ///     let rectangle = Rectangle::new(Point::zero(), Size::new(2, 2));
355    ///     display.fill_rectangle(rectangle, Rgb565::BLACK)?;
356    ///     display.fill_contiguous(rectangle, [Rgb565::RED; 4])?;
357    ///     // One DrawItem, so reserve one prepared-item slot.
358    ///     display.draw_items::<1>(rectangle, Rgb565::BLACK, [
359    ///         DrawItem::Circle {
360    ///             center: (1.0, 1.0), pixel_radius: 1.0, color: Rgb888::WHITE,
361    ///         },
362    ///     ])?;
363    ///     display.clear()?;
364    ///     display.fill(Rgb565::WHITE)
365    /// }
366    /// ```
367    fn fill_rectangle(&mut self, rectangle: Rectangle, color: Rgb565) -> Result<(), Self::Error>;
368
369    /// Fill `rectangle` immediately from row-major native-color pixels.
370    ///
371    /// Empty rectangles are a no-op. Otherwise supply exactly
372    /// `rectangle_pixel_count(rectangle)` pixels: a short iterator leaves the
373    /// remaining pixels untouched, while extra pixels are ignored. This method
374    /// does not infer missing pixels or repeat the final value.
375    ///
376    /// # Example
377    ///
378    /// Stream an image directly when its RGB565 pixels do not need further
379    /// drawing or transformation:
380    ///
381    /// ```rust,no_run
382    /// use device_envoy_core::cyd::{
383    ///     CydDisplay,
384    ///     display::{Image565Fixed, tga},
385    /// };
386    /// use embedded_graphics::{
387    ///     prelude::Point,
388    ///     primitives::Rectangle,
389    /// };
390    ///
391    /// const BITMAP: Image565Fixed<45, 73, { 45 * 73 }> =
392    ///     tga!(concat!(env!("CARGO_MANIFEST_DIR"),
393    ///         "/docs/assets/cyd_fill_contiguous.tga"))
394    ///     .to_565();
395    ///
396    /// fn stream_bitmap<D: CydDisplay>(display: &mut D) -> Result<(), D::Error> {
397    ///     let bitmap = BITMAP.view();
398    ///     let destination = Rectangle::new(Point::new(40, 30), bitmap.size());
399    ///
400    ///     display.fill_contiguous(destination, bitmap.rgb565_iter())
401    /// }
402    /// ```
403    ///
404    /// The `tga!` macro embeds and decodes the file at compile time. The view
405    /// borrows that `const` image, supplies its dimensions, and yields pixels
406    /// in row-major order. The destination can be anywhere on the display, and
407    /// this path requires neither a frame buffer nor heap allocation.
408    ///
409    /// For a whole-screen bitmap, see the
410    /// [`fill_contiguous_full` example](CydDisplay::fill_contiguous_full). See the
411    /// [shared DNS tester's bitmap-streaming code](https://github.com/CarlKCarlK/device-envoy/blob/main/crates/device-envoy-examples-core/src/dns_tester.rs#L377-L381)
412    /// for a complete working example.
413    #[cfg_attr(
414        feature = "doc-images",
415        doc = ::embed_doc_image::embed_image!(
416            "cyd_fill_contiguous_preview",
417            "docs/assets/cyd_fill_contiguous_preview.png"
418        )
419    )]
420    #[cfg_attr(
421        feature = "doc-images",
422        doc = "\n![A bitmap streamed into a region of an in-memory CYD display.][cyd_fill_contiguous_preview]\n"
423    )]
424    fn fill_contiguous<I>(&mut self, rectangle: Rectangle, pixels: I) -> Result<(), Self::Error>
425    where
426        I: IntoIterator<Item = Rgb565>;
427
428    /// Fill the complete screen immediately from row-major native-color pixels.
429    ///
430    /// This is the whole-screen counterpart to [`CydDisplay::fill_contiguous`].
431    /// It expresses full-screen streaming intent without repeating the complete
432    /// screen rectangle. Streaming is an advanced raster path: the caller
433    /// generates every pixel in row-major order rather than drawing a scene.
434    ///
435    /// # Example
436    ///
437    /// ```rust,no_run
438    /// use device_envoy_core::cyd::CydDisplay;
439    /// use embedded_graphics::{pixelcolor::Rgb565, prelude::RgbColor};
440    ///
441    /// fn stream_background<D: CydDisplay>(display: &mut D) -> Result<(), D::Error> {
442    ///     let screen_size = display.screen_size();
443    ///     // A blue-green RGB565 gradient with a warmer lower-right corner.
444    ///     let pixels = (0..screen_size.height).flat_map(|position_y| {
445    ///         (0..screen_size.width).map(move |position_x| {
446    ///             Rgb565::new(
447    ///                 (position_x * 31 / (screen_size.width - 1)) as u8,
448    ///                 (position_y * 63 / (screen_size.height - 1)) as u8,
449    ///                 ((position_x + position_y) * 31
450    ///                     / (screen_size.width + screen_size.height - 2)) as u8,
451    ///             )
452    ///         })
453    ///     });
454    ///     display.fill_contiguous_full(pixels)
455    /// }
456    /// ```
457    ///
458    /// The iterator generates each pixel just before it is sent, without a
459    /// frame buffer or heap allocation. To position a stored bitmap, see the
460    /// [`fill_contiguous` example](CydDisplay::fill_contiguous). The
461    /// [Linkage Blaze clock](https://github.com/CarlKCarlK/linkage-blaze/blob/main/crates/linkage-blaze/src/examples/clock.rs#L148-L151)
462    /// demonstrates full-screen streaming in a complete application.
463    #[cfg_attr(
464        feature = "doc-images",
465        doc = ::embed_doc_image::embed_image!(
466            "cyd_fill_contiguous_full_preview",
467            "docs/assets/cyd_fill_contiguous_full_preview.png"
468        )
469    )]
470    #[cfg_attr(
471        feature = "doc-images",
472        doc = "\n![A numerically generated gradient streamed into an in-memory CYD display.][cyd_fill_contiguous_full_preview]\n"
473    )]
474    fn fill_contiguous_full<I>(&mut self, pixels: I) -> Result<(), Self::Error>
475    where
476        I: IntoIterator<Item = Rgb565>,
477    {
478        self.fill_contiguous(Rectangle::new(Point::zero(), self.screen_size()), pixels)
479    }
480
481    /// Draw `items` immediately inside `bounds`.
482    ///
483    /// See the [immediate-operations example](CydDisplay::fill_rectangle) for a
484    /// complete immediate-drawing flow.
485    /// `DRAW_ITEM_CAPACITY` is the allocation-free capacity for prepared draw
486    /// items. Each nondegenerate item consumes at most one slot, including an
487    /// item that lies outside `bounds`. Using the total number of supplied items
488    /// is always safe.
489    ///
490    /// # Panics
491    ///
492    /// Panics if preparing the items exhausts `DRAW_ITEM_CAPACITY`.
493    fn draw_items<const DRAW_ITEM_CAPACITY: usize>(
494        &mut self,
495        bounds: Rectangle,
496        background_color: Rgb565,
497        items: impl IntoIterator<Item = display::DrawItem>,
498    ) -> Result<(), Self::Error> {
499        let bounds = bounds.intersection(&Rectangle::new(Point::zero(), self.screen_size()));
500        let pixel_sources = ContiguousPixels::<DRAW_ITEM_CAPACITY>::from_draw_items(
501            bounds,
502            background_color,
503            items,
504        );
505        self.fill_contiguous(pixel_sources.bounds(), pixel_sources.iter())
506    }
507
508    /// Clear the whole screen to the device default background color.
509    ///
510    /// New frames already start cleared to this color. This is for immediately
511    /// returning the logical display to the default background between frame
512    /// workflows.
513    ///
514    /// See the [immediate-operations example](CydDisplay::fill_rectangle).
515    fn clear(&mut self) -> Result<(), Self::Error> {
516        self.fill(self.background_565())
517    }
518
519    /// Fill the whole screen with an explicit color.
520    ///
521    /// See the [immediate-operations example](CydDisplay::fill_rectangle).
522    fn fill(&mut self, color: Rgb565) -> Result<(), Self::Error> {
523        self.fill_rectangle(Rectangle::new(Point::zero(), self.screen_size()), color)
524    }
525
526    /// Draw and flush each tile in `grid`.
527    ///
528    /// `draw` receives one frame for each tile. See
529    /// [`CydFrame`](display::CydFrame#coordinates-and-clipping) for how the same
530    /// screen-coordinate scene is clipped to each tile. Each frame is flushed
531    /// after `draw` returns and before the next tile is processed. Only one tile
532    /// is buffered at a time.
533    ///
534    /// See the [`TileGrid`](display::tiling::TileGrid) example for grid
535    /// construction, buffer sizing, and a scene drawn across tile boundaries.
536    fn for_each_tile<'a, F>(
537        &'a mut self,
538        grid: display::tiling::TileGrid,
539        mut draw: F,
540    ) -> impl Future<Output = Result<(), Self::Error>> + 'a
541    where
542        Self: Sized,
543        F: for<'frame> FnMut(&mut Self::Frame<'frame>) + 'a,
544    {
545        async move {
546            let mut tiles = display::tiling::Tiles::new(self, grid);
547            while let Some(mut frame) = tiles.next() {
548                draw(&mut frame);
549                frame.flush().await?;
550            }
551            Ok(())
552        }
553    }
554}
555
556#[cfg(test)]
557mod tests {
558    use super::*;
559    use crate::cyd::display::CydFrame;
560    use crate::pixel_target::PixelTarget;
561    use core::convert::Infallible;
562    use embedded_graphics::pixelcolor::WebColors;
563    use embedded_graphics::{
564        Pixel,
565        prelude::{Dimensions, DrawTarget},
566    };
567
568    // TODO The shared `linkage-blaze-cyd-memory` fake cannot replace this unit-test
569    // double directly because a cyd-core <-> cyd-memory dev-dependency cycle gives
570    // cyd-core's unit tests a second trait instance, so `CydMemory` no longer
571    // implements *this* module's `CydDisplay` trait. Keep this tiny local
572    // test double until the trait crate/test layout is refactored to break that cycle.
573    struct TestCyd;
574
575    struct TestFrame {
576        rectangle: Rectangle,
577    }
578
579    impl backend::DisplayBackend for TestCyd {
580        type Error = Infallible;
581        type Frame<'a> = TestFrame;
582
583        fn create_frame_mut(&mut self, rectangle: Rectangle) -> TestFrame {
584            TestFrame { rectangle }
585        }
586    }
587
588    impl CydDisplay for TestCyd {
589        fn screen_size(&self) -> Size {
590            Size::new(320, 240)
591        }
592
593        fn background_color(&self) -> Rgb888 {
594            Rgb888::CSS_BLACK
595        }
596
597        fn foreground_color(&self) -> Rgb888 {
598            Rgb888::CSS_WHITE
599        }
600
601        fn background_565(&self) -> Rgb565 {
602            self.to_rgb565(self.background_color())
603        }
604
605        fn foreground_565(&self) -> Rgb565 {
606            self.to_rgb565(self.foreground_color())
607        }
608
609        fn fill_rectangle(
610            &mut self,
611            _rectangle: Rectangle,
612            _color: Rgb565,
613        ) -> Result<(), Infallible> {
614            Ok(())
615        }
616
617        fn fill_contiguous<I>(
618            &mut self,
619            _rectangle: Rectangle,
620            _pixels: I,
621        ) -> Result<(), Infallible>
622        where
623            I: IntoIterator<Item = Rgb565>,
624        {
625            Ok(())
626        }
627    }
628
629    impl DrawTarget for TestFrame {
630        type Color = Rgb565;
631        type Error = Infallible;
632
633        fn draw_iter<I>(&mut self, _pixels: I) -> Result<(), Self::Error>
634        where
635            I: IntoIterator<Item = Pixel<Self::Color>>,
636        {
637            Ok(())
638        }
639    }
640
641    impl Dimensions for TestFrame {
642        fn bounding_box(&self) -> Rectangle {
643            self.rectangle
644        }
645    }
646
647    impl PixelTarget for TestFrame {
648        fn width(&self) -> usize {
649            (self.rectangle.top_left.x as usize) + self.rectangle.size.width as usize
650        }
651
652        fn height(&self) -> usize {
653            (self.rectangle.top_left.y as usize) + self.rectangle.size.height as usize
654        }
655
656        fn put_pixel(&mut self, _x: usize, _y: usize, _color: Rgb888) {}
657    }
658
659    impl CydFrame for TestFrame {
660        type Error = Infallible;
661
662        fn rectangle(&self) -> Rectangle {
663            self.rectangle
664        }
665
666        fn fill(&mut self, _color: Rgb565) -> &mut Self {
667            self
668        }
669
670        fn clear(&mut self) -> &mut Self {
671            self
672        }
673
674        fn write_text(&mut self, _text: &str) -> &mut Self {
675            self
676        }
677
678        fn copy_from_565(&mut self, _src: &[u16]) -> crate::Result<()> {
679            Ok(())
680        }
681
682        async fn flush(&mut self) -> Result<(), Infallible> {
683            Ok(())
684        }
685    }
686
687    #[test]
688    fn tiled_frames_use_logical_display_rectangles() {
689        let mut cyd = TestCyd;
690        let grid = display::tiling::TileGrid::new(
691            Rectangle::new(Point::new(10, 20), Size::new(8, 6)),
692            2,
693            2,
694        );
695        let mut tiles = display::tiling::Tiles::new(&mut cyd, grid);
696
697        {
698            let first = tiles.next().expect("first tile exists");
699            assert_eq!(
700                first.rectangle(),
701                Rectangle::new(Point::new(10, 20), Size::new(4, 3))
702            );
703            assert_eq!(first.bounding_box(), first.rectangle());
704        }
705
706        {
707            let second = tiles.next().expect("second tile exists");
708            assert_eq!(
709                second.rectangle(),
710                Rectangle::new(Point::new(14, 20), Size::new(4, 3))
711            );
712            assert_eq!(second.bounding_box(), second.rectangle());
713        }
714
715        let third = tiles.next().expect("third tile exists");
716        assert_eq!(
717            third.rectangle(),
718            Rectangle::new(Point::new(10, 23), Size::new(4, 3))
719        );
720        assert_eq!(third.bounding_box(), third.rectangle());
721    }
722}