Skip to main content

endbasic_std/console/
graphics.rs

1// EndBASIC
2// Copyright 2024 Julio Merino
3//
4// Licensed under the Apache License, Version 2.0 (the "License"); you may not
5// use this file except in compliance with the License.  You may obtain a copy
6// of the License at:
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
13// License for the specific language governing permissions and limitations
14// under the License.
15
16//! Support to implement graphical consoles.
17
18use super::{
19    AnsiColor, CharsXY, ClearType, Console, Key, LineBuffer, PixelsXY, RGB, SizeInPixels,
20    ansi_color_to_rgb, remove_control_chars,
21};
22use async_trait::async_trait;
23use std::convert::TryFrom;
24use std::io;
25
26/// Default foreground color, used at console creation time and when requesting the default color
27/// via the `COLOR` command.
28const DEFAULT_FG_COLOR: u8 = AnsiColor::White as u8;
29
30/// Default background color, used at console creation time and when requesting the default color
31/// via the `COLOR` command.
32const DEFAULT_BG_COLOR: u8 = AnsiColor::Black as u8;
33
34/// Conversion between types with silent value clamping.
35pub trait ClampedInto<T> {
36    /// Converts self into `T` capping values at `T`'s maximum or minimum boundaries.
37    fn clamped_into(self) -> T;
38}
39
40impl ClampedInto<usize> for i16 {
41    fn clamped_into(self) -> usize {
42        if self < 0 { 0 } else { self as usize }
43    }
44}
45
46impl ClampedInto<i16> for u16 {
47    fn clamped_into(self) -> i16 {
48        if self > u16::try_from(i16::MAX).unwrap() { i16::MAX } else { self as i16 }
49    }
50}
51
52impl ClampedInto<i16> for i32 {
53    fn clamped_into(self) -> i16 {
54        if self > i32::from(i16::MAX) {
55            i16::MAX
56        } else if self < i32::from(i16::MIN) {
57            i16::MIN
58        } else {
59            self as i16
60        }
61    }
62}
63
64impl ClampedInto<u16> for i32 {
65    fn clamped_into(self) -> u16 {
66        if self > i32::from(u16::MAX) {
67            u16::MAX
68        } else if self < 0 {
69            0
70        } else {
71            self as u16
72        }
73    }
74}
75
76impl ClampedInto<u16> for u32 {
77    fn clamped_into(self) -> u16 {
78        if self > u32::from(u16::MAX) { u16::MAX } else { self as u16 }
79    }
80}
81
82/// Multiplication of values into a narrower type with silent value clamping.
83pub trait ClampedMul<T, O> {
84    /// Multiplies self by `rhs` and clamps the result to fit in `O`.
85    fn clamped_mul(self, rhs: T) -> O;
86}
87
88impl ClampedMul<u16, i16> for u16 {
89    fn clamped_mul(self, rhs: u16) -> i16 {
90        let product = u32::from(self) * u32::from(rhs);
91        if product > i16::MAX as u32 { i16::MAX } else { product as i16 }
92    }
93}
94
95impl ClampedMul<u16, u16> for u16 {
96    fn clamped_mul(self, rhs: u16) -> u16 {
97        let product = u32::from(self) * u32::from(rhs);
98        if product > u16::MAX as u32 { u16::MAX } else { product as u16 }
99    }
100}
101
102impl ClampedMul<u16, i32> for u16 {
103    fn clamped_mul(self, rhs: u16) -> i32 {
104        i32::from(self).checked_mul(i32::from(rhs)).unwrap_or(i32::MAX)
105    }
106}
107
108impl ClampedMul<u16, u32> for u16 {
109    fn clamped_mul(self, rhs: u16) -> u32 {
110        u32::from(self).checked_mul(u32::from(rhs)).expect("Result must have fit")
111    }
112}
113
114impl ClampedMul<usize, usize> for usize {
115    fn clamped_mul(self, rhs: usize) -> usize {
116        match self.checked_mul(rhs) {
117            Some(v) => v,
118            None => usize::MAX,
119        }
120    }
121}
122
123impl ClampedMul<SizeInPixels, PixelsXY> for CharsXY {
124    fn clamped_mul(self, rhs: SizeInPixels) -> PixelsXY {
125        PixelsXY { x: self.x.clamped_mul(rhs.width), y: self.y.clamped_mul(rhs.height) }
126    }
127}
128
129/// Given two points, calculates the origin and size of the rectangle they define.
130fn rect_points(x1y1: PixelsXY, x2y2: PixelsXY) -> Option<(PixelsXY, SizeInPixels)> {
131    let (x1, x2) = if x1y1.x < x2y2.x { (x1y1.x, x2y2.x) } else { (x2y2.x, x1y1.x) };
132    let (y1, y2) = if x1y1.y < x2y2.y { (x1y1.y, x2y2.y) } else { (x2y2.y, x1y1.y) };
133
134    let width = {
135        let width = i32::from(x2) - i32::from(x1);
136        if cfg!(debug_assertions) {
137            u32::try_from(width).expect("Width must have been non-negative")
138        } else {
139            width as u32
140        }
141    }
142    .clamped_into();
143    let height = {
144        let height = i32::from(y2) - i32::from(y1);
145        if cfg!(debug_assertions) {
146            u32::try_from(height).expect("Height must have been non-negative")
147        } else {
148            height as u32
149        }
150    }
151    .clamped_into();
152
153    if width == 0 || height == 0 {
154        None
155    } else {
156        Some((PixelsXY::new(x1, y1), SizeInPixels::new(width, height)))
157    }
158}
159
160/// Container for configuration information of the backing surface.
161pub struct RasterInfo {
162    /// Size of the console in pixels.
163    pub size_pixels: SizeInPixels,
164
165    /// Size of each character.
166    pub glyph_size: SizeInPixels,
167
168    /// Size of the console in characters.  This is derived from `size_pixels` and `glyph_size`.
169    pub size_chars: CharsXY,
170}
171
172/// Primitive graphical console raster operations.
173pub trait RasterOps {
174    /// Type of the image data (raw pixels).
175    type ID;
176
177    /// Queries information about the backend.
178    fn get_info(&self) -> RasterInfo;
179
180    /// Sets the drawing color for subsequent operations.
181    fn set_draw_color(&mut self, color: RGB);
182
183    /// Clears the whole console with the given color.
184    fn clear(&mut self) -> io::Result<()>;
185
186    /// Sets whether automatic presentation of the canvas is enabled or not.
187    ///
188    /// Raster backends might need this when the device they talk to is very slow and they want to
189    /// buffer data in main memory first.
190    ///
191    /// Does *NOT* present the canvas.
192    fn set_sync(&mut self, _enabled: bool) {}
193
194    /// Displays any buffered changes to the console.
195    ///
196    /// Should ignore any sync values that the backend might have cached via `set_sync`.
197    fn present_canvas(&mut self) -> io::Result<()>;
198
199    /// Reads the raw pixel data for the rectangular region specified by `xy` and `size`.
200    fn read_pixels(&mut self, xy: PixelsXY, size: SizeInPixels) -> io::Result<Self::ID>;
201
202    /// Restores the rectangular region stored in `data` at the `xy` coordinates.
203    fn put_pixels(&mut self, xy: PixelsXY, data: &Self::ID) -> io::Result<()>;
204
205    /// Moves the rectangular region specified by `x1y1` and `size` to `x2y2`.  The original region
206    /// is erased with the current drawing color.
207    fn move_pixels(&mut self, x1y1: PixelsXY, x2y2: PixelsXY, size: SizeInPixels)
208    -> io::Result<()>;
209
210    /// Writes `text` starting at `xy` with the current drawing color.
211    fn write_text(&mut self, xy: PixelsXY, text: &str) -> io::Result<()>;
212
213    /// Draws the outline of a circle at `center` with `radius` using the current drawing color.
214    fn draw_circle(&mut self, center: PixelsXY, radius: u16) -> io::Result<()>;
215
216    /// Draws a filled circle at `center` with `radius` using the current drawing color.
217    fn draw_circle_filled(&mut self, center: PixelsXY, radius: u16) -> io::Result<()>;
218
219    /// Draws a line from `x1y1` to `x2y2` using the current drawing color.
220    fn draw_line(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()>;
221
222    /// Draws a single pixel at `xy` using the current drawing color.
223    fn draw_pixel(&mut self, xy: PixelsXY) -> io::Result<()>;
224
225    /// Draws the outline of a rectangle from `x1y1` to `x2y2` using the current drawing color.
226    fn draw_rect(&mut self, xy: PixelsXY, size: SizeInPixels) -> io::Result<()>;
227
228    /// Draws a filled rectangle from `x1y1` to `x2y2` using the current drawing color.
229    fn draw_rect_filled(&mut self, xy: PixelsXY, size: SizeInPixels) -> io::Result<()>;
230}
231
232/// Primitive graphical console input operations.
233#[async_trait(?Send)]
234pub trait InputOps {
235    /// Returns the next key press if any is available.
236    async fn poll_key(&mut self) -> io::Result<Option<Key>>;
237
238    /// Waits for and returns the next key press.
239    async fn read_key(&mut self) -> io::Result<Key>;
240}
241
242/// Implementation of a console that renders to a backing surface.
243pub struct GraphicsConsole<IO, RO>
244where
245    RO: RasterOps,
246{
247    input_ops: IO,
248
249    /// Operations to render to the console.
250    raster_ops: RO,
251
252    /// Size of the console in pixels.
253    size_pixels: SizeInPixels,
254
255    /// Size of each character.
256    glyph_size: SizeInPixels,
257
258    /// Size of the console in characters.  This is derived from `size_pixels` and `glyph_size`.
259    size_chars: CharsXY,
260
261    /// Location of the cursor.
262    cursor_pos: CharsXY,
263
264    /// Whether the cursor is visible or not.
265    cursor_visible: bool,
266
267    /// Raw pixels at the cursor position before the cursor was drawn.  Used to restore the previous
268    /// contents when the cursor moves.
269    cursor_backup: Option<RO::ID>,
270
271    /// Default foreground color to use.
272    default_fg_color: u8,
273
274    /// Default background color to use.
275    default_bg_color: u8,
276
277    /// Current foreground color as exposed via `color` and `set_color`.
278    ansi_fg_color: Option<u8>,
279
280    /// Current background color as exposed via `color` and `set_color`.
281    ansi_bg_color: Option<u8>,
282
283    /// Current foreground color.  Used for text and graphical rendering.
284    fg_color: RGB,
285
286    /// Current background color.  Used to clear text.
287    bg_color: RGB,
288
289    /// State of the console right before entering the "alternate" console.
290    #[allow(clippy::type_complexity)]
291    alt_backup: Option<(RO::ID, CharsXY, Option<u8>, Option<u8>, RGB, RGB)>,
292
293    /// Whether video syncing is enabled or not.
294    sync_enabled: bool,
295}
296
297impl<IO, RO> GraphicsConsole<IO, RO>
298where
299    IO: InputOps,
300    RO: RasterOps,
301{
302    /// Initializes a new graphical console.
303    pub fn new(
304        input_ops: IO,
305        raster_ops: RO,
306        default_fg_color: Option<u8>,
307        default_bg_color: Option<u8>,
308    ) -> io::Result<Self> {
309        let info = raster_ops.get_info();
310
311        let default_fg_color = default_fg_color.unwrap_or(DEFAULT_FG_COLOR);
312        let default_bg_color = default_bg_color.unwrap_or(DEFAULT_BG_COLOR);
313
314        let mut console = Self {
315            input_ops,
316            raster_ops,
317            size_pixels: info.size_pixels,
318            glyph_size: info.glyph_size,
319            size_chars: info.size_chars,
320            cursor_pos: CharsXY::default(),
321            cursor_visible: true,
322            cursor_backup: None,
323            default_fg_color,
324            default_bg_color,
325            ansi_bg_color: None,
326            ansi_fg_color: None,
327            bg_color: ansi_color_to_rgb(default_bg_color),
328            fg_color: ansi_color_to_rgb(default_fg_color),
329            alt_backup: None,
330            sync_enabled: true,
331        };
332
333        console.set_color(console.ansi_fg_color, console.ansi_bg_color)?;
334        console.clear(ClearType::All)?;
335
336        Ok(console)
337    }
338
339    /// Renders any buffered changes to the backing surface.
340    fn present_canvas(&mut self) -> io::Result<()> {
341        if self.sync_enabled { self.raster_ops.present_canvas() } else { Ok(()) }
342    }
343
344    /// Draws the cursor at the current position and saves the previous contents of the screen so
345    /// that `clear_cursor` can restore them.
346    ///
347    /// Does not present the canvas.
348    fn draw_cursor(&mut self) -> io::Result<()> {
349        if !self.cursor_visible {
350            return Ok(());
351        }
352
353        let x1y1 = self.cursor_pos.clamped_mul(self.glyph_size);
354
355        assert!(self.cursor_backup.is_none());
356        self.cursor_backup = Some(self.raster_ops.read_pixels(x1y1, self.glyph_size)?);
357
358        // TODO(jmmv): It would be nice to draw the cursor with alpha blending so that the letters
359        // under it are visible.  This was done before in the HTML canvas but was lost when I added
360        // the GraphicsConsole abstraction.  Maybe all RGB colors should switch to RGBA.  Or maybe
361        // we should special-case the cursor drawing.
362        self.raster_ops.set_draw_color(self.fg_color);
363        self.raster_ops.draw_rect_filled(x1y1, self.glyph_size)
364    }
365
366    /// Clears the cursor at the current position by restoring the contents of the screen saved by
367    /// an earlier call to `draw_cursor`.
368    ///
369    /// Does not present the canvas.
370    fn clear_cursor(&mut self) -> io::Result<()> {
371        if !self.cursor_visible || self.cursor_backup.is_none() {
372            return Ok(());
373        }
374
375        let x1y1 = self.cursor_pos.clamped_mul(self.glyph_size);
376
377        self.raster_ops.put_pixels(x1y1, self.cursor_backup.as_ref().unwrap())?;
378        self.cursor_backup = None;
379        Ok(())
380    }
381
382    /// Moves the cursor to beginning of the next line, scrolling the console if necessary.
383    ///
384    /// Does not clear nor draw the cursor.
385    fn open_line(&mut self) -> io::Result<()> {
386        if self.cursor_pos.y < self.size_chars.y - 1 {
387            self.cursor_pos.x = 0;
388            self.cursor_pos.y += 1;
389            return Ok(());
390        }
391
392        let x1y1 = PixelsXY::new(0, self.glyph_size.height.clamped_into());
393        let x2y2 = PixelsXY::new(0, 0);
394        let size = SizeInPixels::new(
395            self.size_pixels.width,
396            self.size_pixels.height - self.glyph_size.height,
397        );
398
399        self.raster_ops.set_draw_color(self.bg_color);
400        self.raster_ops.move_pixels(x1y1, x2y2, size)?;
401
402        self.cursor_pos.x = 0;
403        Ok(())
404    }
405
406    /// Renders the given text at the current cursor position, with wrapping and
407    /// scrolling if necessary.
408    fn raw_write_wrapped(&mut self, text: String) -> io::Result<()> {
409        let mut line_buffer = LineBuffer::from(text);
410
411        loop {
412            let fit_chars = self.size_chars.x - self.cursor_pos.x;
413
414            let remaining = line_buffer.split_off(usize::from(fit_chars));
415            let len = match u16::try_from(line_buffer.len()) {
416                Ok(len) => len,
417                Err(_) => return Err(io::Error::new(io::ErrorKind::InvalidInput, "Text too long")),
418            };
419
420            if len > 0 {
421                let xy = self.cursor_pos.clamped_mul(self.glyph_size);
422                let size = SizeInPixels::new(
423                    len.clamped_mul(self.glyph_size.width),
424                    self.glyph_size.height,
425                );
426
427                self.raster_ops.set_draw_color(self.bg_color);
428                self.raster_ops.draw_rect_filled(xy, size)?;
429
430                self.raster_ops.set_draw_color(self.fg_color);
431                self.raster_ops.write_text(xy, &line_buffer.into_inner())?;
432                self.cursor_pos.x += len;
433            }
434
435            line_buffer = remaining;
436            if line_buffer.is_empty() {
437                break;
438            } else {
439                self.open_line()?;
440            }
441        }
442
443        Ok(())
444    }
445}
446
447#[async_trait(?Send)]
448impl<IO, RO> Console for GraphicsConsole<IO, RO>
449where
450    IO: InputOps,
451    RO: RasterOps,
452{
453    fn clear(&mut self, how: ClearType) -> io::Result<()> {
454        match how {
455            ClearType::All => {
456                self.raster_ops.set_draw_color(self.bg_color);
457                self.raster_ops.clear()?;
458                self.cursor_pos.y = 0;
459                self.cursor_pos.x = 0;
460                self.cursor_backup = None;
461            }
462            ClearType::CurrentLine => {
463                self.clear_cursor()?;
464                let xy = PixelsXY::new(0, self.cursor_pos.y.clamped_mul(self.glyph_size.height));
465                let size = SizeInPixels::new(self.size_pixels.width, self.glyph_size.height);
466                self.raster_ops.set_draw_color(self.bg_color);
467                self.raster_ops.draw_rect_filled(xy, size)?;
468                self.cursor_pos.x = 0;
469            }
470            ClearType::PreviousChar => {
471                if self.cursor_pos.x > 0 {
472                    self.clear_cursor()?;
473                    let previous_pos = CharsXY::new(self.cursor_pos.x - 1, self.cursor_pos.y);
474                    let origin = previous_pos.clamped_mul(self.glyph_size);
475                    self.raster_ops.set_draw_color(self.bg_color);
476                    self.raster_ops.draw_rect_filled(origin, self.glyph_size)?;
477                    self.cursor_pos = previous_pos;
478                }
479            }
480            ClearType::UntilNewLine => {
481                self.clear_cursor()?;
482                let pos = self.cursor_pos.clamped_mul(self.glyph_size);
483                debug_assert!(pos.x >= 0, "Inputs to pos are unsigned");
484                debug_assert!(pos.y >= 0, "Inputs to pos are unsigned");
485                let size = SizeInPixels::new(
486                    (i32::from(self.size_pixels.width) - i32::from(pos.x)).clamped_into(),
487                    self.glyph_size.height,
488                );
489                self.raster_ops.set_draw_color(self.bg_color);
490                self.raster_ops.draw_rect_filled(pos, size)?;
491            }
492        }
493        self.draw_cursor()?;
494        self.present_canvas()
495    }
496
497    fn color(&self) -> (Option<u8>, Option<u8>) {
498        (self.ansi_fg_color, self.ansi_bg_color)
499    }
500
501    fn set_color(&mut self, fg: Option<u8>, bg: Option<u8>) -> io::Result<()> {
502        self.ansi_fg_color = fg;
503        self.fg_color = ansi_color_to_rgb(fg.unwrap_or(self.default_fg_color));
504        self.ansi_bg_color = bg;
505        self.bg_color = ansi_color_to_rgb(bg.unwrap_or(self.default_bg_color));
506        Ok(())
507    }
508
509    fn enter_alt(&mut self) -> io::Result<()> {
510        if self.alt_backup.is_some() {
511            return Err(io::Error::new(
512                io::ErrorKind::InvalidInput,
513                "Cannot nest alternate screens",
514            ));
515        }
516
517        let pixels = self.raster_ops.read_pixels(PixelsXY::new(0, 0), self.size_pixels)?;
518        self.alt_backup = Some((
519            pixels,
520            self.cursor_pos,
521            self.ansi_fg_color,
522            self.ansi_bg_color,
523            self.fg_color,
524            self.bg_color,
525        ));
526
527        self.clear(ClearType::All)
528    }
529
530    fn hide_cursor(&mut self) -> io::Result<()> {
531        self.clear_cursor()?;
532        self.cursor_visible = false;
533        self.present_canvas()
534    }
535
536    fn is_interactive(&self) -> bool {
537        true
538    }
539
540    fn leave_alt(&mut self) -> io::Result<()> {
541        let (pixels, cursor_pos, ansi_fg_color, ansi_bg_color, fg_color, bg_color) =
542            match self.alt_backup.take() {
543                Some(t) => t,
544                None => {
545                    return Err(io::Error::new(
546                        io::ErrorKind::InvalidInput,
547                        "Cannot leave alternate screen; not entered",
548                    ));
549                }
550            };
551
552        self.clear_cursor()?;
553
554        self.raster_ops.put_pixels(PixelsXY::new(0, 0), &pixels)?;
555
556        self.cursor_pos = cursor_pos;
557        self.ansi_fg_color = ansi_fg_color;
558        self.ansi_bg_color = ansi_bg_color;
559        self.fg_color = fg_color;
560        self.bg_color = bg_color;
561        self.draw_cursor()?;
562        self.present_canvas()?;
563
564        debug_assert!(self.alt_backup.is_none());
565        Ok(())
566    }
567
568    fn locate(&mut self, pos: CharsXY) -> io::Result<()> {
569        debug_assert!(pos.x < self.size_chars.x);
570        debug_assert!(pos.y < self.size_chars.y);
571
572        let previous = self.set_sync(false)?;
573        self.clear_cursor()?;
574        self.cursor_pos = pos;
575        self.draw_cursor()?;
576        self.set_sync(previous)?;
577        Ok(())
578    }
579
580    fn move_within_line(&mut self, off: i16) -> io::Result<()> {
581        let previous = self.set_sync(false)?;
582        self.clear_cursor()?;
583        if off < 0 {
584            self.cursor_pos.x -= -off as u16;
585        } else {
586            self.cursor_pos.x += off as u16;
587        }
588        self.draw_cursor()?;
589        self.set_sync(previous)?;
590        Ok(())
591    }
592
593    fn print(&mut self, text: &str) -> io::Result<()> {
594        let text = remove_control_chars(text);
595
596        let previous = self.set_sync(false)?;
597        self.clear_cursor()?;
598        self.raw_write_wrapped(text)?;
599        self.open_line()?;
600        self.draw_cursor()?;
601        self.set_sync(previous)?;
602        Ok(())
603    }
604
605    async fn poll_key(&mut self) -> io::Result<Option<Key>> {
606        self.input_ops.poll_key().await
607    }
608
609    async fn read_key(&mut self) -> io::Result<Key> {
610        self.input_ops.read_key().await
611    }
612
613    fn show_cursor(&mut self) -> io::Result<()> {
614        if !self.cursor_visible {
615            self.cursor_visible = true;
616            if let Err(e) = self.draw_cursor() {
617                self.cursor_visible = false;
618                return Err(e);
619            }
620        }
621        self.present_canvas()
622    }
623
624    fn size_chars(&self) -> io::Result<CharsXY> {
625        Ok(self.size_chars)
626    }
627
628    fn size_pixels(&self) -> io::Result<SizeInPixels> {
629        Ok(self.size_pixels)
630    }
631
632    fn write(&mut self, text: &str) -> io::Result<()> {
633        let text = remove_control_chars(text);
634
635        let previous = self.set_sync(false)?;
636        self.clear_cursor()?;
637        self.raw_write_wrapped(text)?;
638        self.draw_cursor()?;
639        self.set_sync(previous)?;
640        Ok(())
641    }
642
643    fn draw_circle(&mut self, center: PixelsXY, radius: u16) -> io::Result<()> {
644        self.raster_ops.set_draw_color(self.fg_color);
645        self.raster_ops.draw_circle(center, radius)?;
646        self.present_canvas()
647    }
648
649    fn draw_circle_filled(&mut self, center: PixelsXY, radius: u16) -> io::Result<()> {
650        self.raster_ops.set_draw_color(self.fg_color);
651        self.raster_ops.draw_circle_filled(center, radius)?;
652        self.present_canvas()
653    }
654
655    fn draw_line(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()> {
656        self.raster_ops.set_draw_color(self.fg_color);
657        self.raster_ops.draw_line(x1y1, x2y2)?;
658        self.present_canvas()
659    }
660
661    fn draw_pixel(&mut self, xy: PixelsXY) -> io::Result<()> {
662        self.raster_ops.set_draw_color(self.fg_color);
663        self.raster_ops.draw_pixel(xy)?;
664        self.present_canvas()
665    }
666
667    fn draw_rect(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()> {
668        self.raster_ops.set_draw_color(self.fg_color);
669        match rect_points(x1y1, x2y2) {
670            Some((xy, size)) => self.raster_ops.draw_rect(xy, size)?,
671            None => self.raster_ops.draw_line(x1y1, x2y2)?,
672        }
673        self.present_canvas()
674    }
675
676    fn draw_rect_filled(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()> {
677        self.raster_ops.set_draw_color(self.fg_color);
678        match rect_points(x1y1, x2y2) {
679            Some((xy, size)) => self.raster_ops.draw_rect_filled(xy, size)?,
680            None => self.raster_ops.draw_line(x1y1, x2y2)?,
681        }
682        self.present_canvas()
683    }
684
685    fn sync_now(&mut self) -> io::Result<()> {
686        if self.sync_enabled { Ok(()) } else { self.raster_ops.present_canvas() }
687    }
688
689    fn set_sync(&mut self, enabled: bool) -> io::Result<bool> {
690        if !self.sync_enabled && enabled {
691            self.raster_ops.present_canvas()?;
692        }
693        let previous = self.sync_enabled;
694        self.sync_enabled = enabled;
695        self.raster_ops.set_sync(enabled);
696        Ok(previous)
697    }
698}
699
700#[cfg(test)]
701mod tests {
702    use super::*;
703
704    #[test]
705    fn test_clamped_into_u16_i16() {
706        assert_eq!(0i16, 0u16.clamped_into());
707        assert_eq!(10i16, 10u16.clamped_into());
708        assert_eq!(i16::MAX - 1, u16::try_from(i16::MAX - 1).unwrap().clamped_into());
709        assert_eq!(i16::MAX, u16::try_from(i16::MAX).unwrap().clamped_into());
710        assert_eq!(i16::MAX, u16::MAX.clamped_into());
711    }
712
713    #[test]
714    fn test_clamped_into_u16_i32() {
715        assert_eq!(0i16, 0i32.clamped_into());
716        assert_eq!(10i16, 10i32.clamped_into());
717        assert_eq!(i16::MIN + 1, i32::from(i16::MIN + 1).clamped_into());
718        assert_eq!(i16::MIN, i32::from(i16::MIN).clamped_into());
719        assert_eq!(i16::MIN, i32::MIN.clamped_into());
720        assert_eq!(i16::MAX - 1, i32::from(i16::MAX - 1).clamped_into());
721        assert_eq!(i16::MAX, i32::from(i16::MAX).clamped_into());
722        assert_eq!(i16::MAX, i32::MAX.clamped_into());
723    }
724
725    #[test]
726    fn test_clamped_into_i32_u16() {
727        assert_eq!(0u16, 0i32.clamped_into());
728        assert_eq!(10u16, 10i32.clamped_into());
729        assert_eq!(0u16, (-10i32).clamped_into());
730        assert_eq!(u16::MAX - 1, i32::from(u16::MAX - 1).clamped_into());
731        assert_eq!(u16::MAX, i32::from(u16::MAX).clamped_into());
732        assert_eq!(u16::MAX, i32::MAX.clamped_into());
733    }
734
735    #[test]
736    fn test_clamped_into_u32_u16() {
737        assert_eq!(0u16, 0u32.clamped_into());
738        assert_eq!(10u16, 10u32.clamped_into());
739        assert_eq!(u16::MAX - 1, u32::from(u16::MAX - 1).clamped_into());
740        assert_eq!(u16::MAX, u32::from(u16::MAX).clamped_into());
741        assert_eq!(u16::MAX, u32::MAX.clamped_into());
742    }
743
744    #[test]
745    fn test_clamped_mul_u16_u16_i16() {
746        assert_eq!(0i16, ClampedMul::<u16, i16>::clamped_mul(0u16, 0u16));
747        assert_eq!(55i16, ClampedMul::<u16, i16>::clamped_mul(11u16, 5u16));
748        assert_eq!(i16::MAX, ClampedMul::<u16, i16>::clamped_mul(u16::MAX, u16::MAX));
749    }
750
751    #[test]
752    fn test_clamped_mul_u16_u16_u16() {
753        assert_eq!(0u16, ClampedMul::<u16, u16>::clamped_mul(0u16, 0u16));
754        assert_eq!(55u16, ClampedMul::<u16, u16>::clamped_mul(11u16, 5u16));
755        assert_eq!(u16::MAX, ClampedMul::<u16, u16>::clamped_mul(u16::MAX, u16::MAX));
756    }
757
758    #[test]
759    fn test_clamped_mul_u16_u16_i32() {
760        assert_eq!(0i32, ClampedMul::<u16, i32>::clamped_mul(0u16, 0u16));
761        assert_eq!(55i32, ClampedMul::<u16, i32>::clamped_mul(11u16, 5u16));
762        assert_eq!(i32::MAX, ClampedMul::<u16, i32>::clamped_mul(u16::MAX, u16::MAX));
763    }
764
765    #[test]
766    fn test_clamped_mul_u16_u16_u32() {
767        assert_eq!(0u32, ClampedMul::<u16, u32>::clamped_mul(0u16, 0u16));
768        assert_eq!(55u32, ClampedMul::<u16, u32>::clamped_mul(11u16, 5u16));
769        assert_eq!(4294836225u32, ClampedMul::<u16, u32>::clamped_mul(u16::MAX, u16::MAX));
770    }
771
772    #[test]
773    fn test_clamped_mul_usize_usize_usize() {
774        assert_eq!(0, ClampedMul::<usize, usize>::clamped_mul(0, 0));
775        assert_eq!(55, ClampedMul::<usize, usize>::clamped_mul(11, 5));
776        assert_eq!(usize::MAX, ClampedMul::<usize, usize>::clamped_mul(usize::MAX, usize::MAX));
777    }
778
779    #[test]
780    fn test_clamped_mul_charsxy_sizeinpixels_pixelsxy() {
781        assert_eq!(
782            PixelsXY { x: 0, y: 0 },
783            CharsXY { x: 0, y: 0 }.clamped_mul(SizeInPixels::new(1, 1))
784        );
785        assert_eq!(
786            PixelsXY { x: 50, y: 120 },
787            CharsXY { x: 10, y: 20 }.clamped_mul(SizeInPixels::new(5, 6))
788        );
789        assert_eq!(
790            PixelsXY { x: i16::MAX, y: 120 },
791            CharsXY { x: 10, y: 20 }.clamped_mul(SizeInPixels::new(50000, 6))
792        );
793        assert_eq!(
794            PixelsXY { x: 50, y: i16::MAX },
795            CharsXY { x: 10, y: 20 }.clamped_mul(SizeInPixels::new(5, 60000))
796        );
797        assert_eq!(
798            PixelsXY { x: i16::MAX, y: i16::MAX },
799            CharsXY { x: 10, y: 20 }.clamped_mul(SizeInPixels::new(50000, 60000))
800        );
801    }
802
803    #[test]
804    fn test_rect_points_ok() {
805        assert_eq!(
806            Some((PixelsXY { x: 10, y: 20 }, SizeInPixels::new(100, 200))),
807            rect_points(PixelsXY { x: 10, y: 20 }, PixelsXY { x: 110, y: 220 })
808        );
809        assert_eq!(
810            Some((PixelsXY { x: 10, y: 20 }, SizeInPixels::new(100, 200))),
811            rect_points(PixelsXY { x: 110, y: 20 }, PixelsXY { x: 10, y: 220 })
812        );
813        assert_eq!(
814            Some((PixelsXY { x: 10, y: 20 }, SizeInPixels::new(100, 200))),
815            rect_points(PixelsXY { x: 10, y: 220 }, PixelsXY { x: 110, y: 20 })
816        );
817        assert_eq!(
818            Some((PixelsXY { x: 10, y: 20 }, SizeInPixels::new(100, 200))),
819            rect_points(PixelsXY { x: 110, y: 220 }, PixelsXY { x: 10, y: 20 })
820        );
821
822        assert_eq!(
823            Some((PixelsXY { x: -31000, y: -32000 }, SizeInPixels::new(31005, 32010))),
824            rect_points(PixelsXY { x: 5, y: -32000 }, PixelsXY { x: -31000, y: 10 })
825        );
826        assert_eq!(
827            Some((PixelsXY { x: 10, y: 5 }, SizeInPixels::new(30990, 31995))),
828            rect_points(PixelsXY { x: 31000, y: 5 }, PixelsXY { x: 10, y: 32000 })
829        );
830
831        assert_eq!(
832            Some((PixelsXY { x: -31000, y: -32000 }, SizeInPixels::new(62000, 64000))),
833            rect_points(PixelsXY { x: -31000, y: -32000 }, PixelsXY { x: 31000, y: 32000 })
834        );
835        assert_eq!(
836            Some((PixelsXY { x: -31000, y: -32000 }, SizeInPixels::new(62000, 64000))),
837            rect_points(PixelsXY { x: 31000, y: 32000 }, PixelsXY { x: -31000, y: -32000 })
838        );
839    }
840
841    #[test]
842    fn test_rect_points_zeroes() {
843        assert_eq!(None, rect_points(PixelsXY { x: 10, y: 10 }, PixelsXY { x: 10, y: 10 }));
844        assert_eq!(None, rect_points(PixelsXY { x: 10, y: 10 }, PixelsXY { x: 10, y: 20 }));
845        assert_eq!(None, rect_points(PixelsXY { x: 10, y: 10 }, PixelsXY { x: 20, y: 10 }));
846    }
847}