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}