Skip to main content

buffer_graphics_lib/
drawing.rs

1use crate::clipping::Clip;
2use crate::drawable::{DrawType, Drawable};
3use crate::image::Image;
4use crate::prelude::PixelFont;
5use crate::shapes::CreateDrawable;
6use crate::text::format::TextFormat;
7use crate::text::pos::TextPos;
8use crate::text::{chr_to_code, Text};
9use crate::GraphicsBuffer::RgbaU8;
10use crate::{Graphics, GraphicsBuffer, GraphicsError};
11use graphics_shapes::circle::Circle;
12use graphics_shapes::coord::Coord;
13use graphics_shapes::polygon::Polygon;
14use graphics_shapes::prelude::Ellipse;
15use graphics_shapes::rect::Rect;
16use graphics_shapes::triangle::Triangle;
17use ici_files::palette::simplify_palette_to_fit;
18use ici_files::prelude::*;
19use std::collections::{HashMap, HashSet};
20use std::mem::swap;
21
22/// Represents anything that [Graphics] can render
23pub trait Renderable<T> {
24    fn render(&self, graphics: &mut Graphics);
25}
26
27#[inline]
28pub(crate) fn index_u8(width: usize, x: usize, y: usize) -> usize {
29    (x + y * width) * 4
30}
31
32#[inline]
33pub(crate) fn index_u32(width: usize, x: usize, y: usize) -> usize {
34    x + y * width
35}
36
37pub(crate) fn clear_u8(buffer: &mut GraphicsBuffer, color: Color) {
38    if let RgbaU8(buffer) = buffer {
39        buffer.chunks_exact_mut(4).for_each(|px| {
40            px[0] = color.r;
41            px[1] = color.g;
42            px[2] = color.b;
43            px[3] = color.a;
44        });
45    } else {
46        panic!(
47            "clear_u8 called on non u8 buffer, please create GitHub issue for buffer-graphics-lib"
48        )
49    }
50}
51
52pub(crate) fn clear_u32(buffer: &mut GraphicsBuffer, color: Color) {
53    #[allow(clippy::type_complexity)] //it's internal to this method
54    let result: Option<(&mut &mut [u32], fn(Color) -> u32)> = match buffer {
55        RgbaU8(_) => None,
56        GraphicsBuffer::RgbaU32(buf) => Some((buf, Color::to_rgba)),
57        GraphicsBuffer::ArgbU32(buf) => Some((buf, Color::to_argb)),
58    };
59    if let Some((buffer, method)) = result {
60        let color = method(color);
61        buffer.iter_mut().for_each(|p| *p = color);
62    } else {
63        panic!("clear_u32 called on non u32 buffer, please create GitHub issue for buffer-graphics-lib")
64    }
65}
66
67impl Graphics<'_> {
68    /// Convert an x,y coord to idx for use with `self.pixels`
69    #[inline(always)]
70    pub fn index(&self, x: usize, y: usize) -> usize {
71        (self.index_method)(self.width, x, y)
72    }
73
74    #[inline(always)]
75    pub fn width(&self) -> usize {
76        self.width
77    }
78
79    #[inline(always)]
80    pub fn height(&self) -> usize {
81        self.height
82    }
83
84    pub fn is_on_screen(&self, point: Coord) -> bool {
85        let x = point.x - self.translate.x;
86        let y = point.y - self.translate.y;
87        x >= 0 && y >= 0 && x < self.width as isize && y < self.height as isize
88    }
89}
90
91impl Graphics<'_> {
92    /// Get the canvas offset in pixels
93    #[inline(always)]
94    pub fn get_translate(&self) -> Coord {
95        self.translate
96    }
97
98    /// Set the canvas offset in pixels
99    ///
100    /// All drawing commands will be offset by this value
101    ///
102    /// # Returns
103    /// The previous translation value
104    #[inline]
105    pub fn set_translate(&mut self, new_value: Coord) -> Coord {
106        let old = self.translate;
107        self.translate = new_value;
108        old
109    }
110
111    #[inline]
112    pub fn with_translate<F: Fn(&mut Graphics)>(&mut self, set: Coord, method: F) {
113        let old_trans = self.set_translate(set);
114        method(self);
115        self.set_translate(old_trans);
116    }
117
118    /// Adds `delta` to the current canvas offset
119    #[inline]
120    pub fn update_translate(&mut self, delta: Coord) {
121        self.translate.x += delta.x;
122        self.translate.y += delta.y;
123    }
124
125    /// Copy entire pixels array to an image
126    pub fn copy_to_image(&self) -> Image {
127        let pixels = self.buffer.to_pixels();
128        Image::new(pixels, self.width, self.height)
129            .expect("Copy to image failed, please create GitHub issue for buffer-graphics-lib")
130    }
131
132    /// Copy entire pixels array to an indexed image
133    /// `simplify_palette` if true and there's more than 255 colours, this will simplify/merge the palette until there are under 255 colours
134    ///
135    /// # Errors
136    ///
137    /// * `GraphicsError::TooManyColors` Over 255 colors have been used and `simplify_palette` was false
138    /// * `GraphicsError::TooBig` Image is bigger than 255x255
139    /// * `GraphicsError::ImageError` Something went wrong creating the IndexedImage
140    pub fn copy_to_indexed_image(
141        &self,
142        simplify_palette: bool,
143    ) -> Result<IndexedImage, GraphicsError> {
144        if self.width > 255 || self.height > 255 {
145            return Err(GraphicsError::TooBig(self.width, self.height));
146        }
147        let width = self.width as u8;
148        let height = self.height as u8;
149        let pixels = self.buffer.to_pixels();
150        let colors: HashSet<Color> = HashSet::from_iter(pixels.iter().copied());
151        let colors = if colors.len() > 255 {
152            if simplify_palette {
153                simplify_palette_to_fit(&colors.into_iter().collect::<Vec<Color>>(), 255)
154            } else {
155                return Err(GraphicsError::TooManyColors);
156            }
157        } else {
158            colors.into_iter().collect()
159        };
160
161        let color_index: HashMap<Color, u8> = colors
162            .iter()
163            .enumerate()
164            .map(|(i, c)| (*c, i as u8))
165            .collect();
166
167        let pixels = pixels
168            .iter()
169            .map(|c| {
170                *color_index.get(c).unwrap_or_else(|| {
171                    panic!("color disappeared, please create GitHub issue for buffer-graphics-lib")
172                })
173            })
174            .collect();
175
176        IndexedImage::new(width, height, colors, pixels).map_err(GraphicsError::ImageError)
177    }
178
179    /// Get top left pixel coord for letter col row
180    pub fn get_px_for_char(col: usize, row: usize, font: &PixelFont) -> (usize, usize) {
181        (col * font.char_width(), row * font.line_height())
182    }
183
184    /// Draw an image at `x`, `y`
185    /// If the image definitely will draw inside the window you can use [draw_image_unchecked] instead
186    pub fn draw_image<P: Into<Coord>>(&mut self, xy: P, image: &Image) {
187        let xy = xy.into();
188        let mut x = 0;
189        let mut y = 0;
190        for pixel in image.pixels() {
191            update_pixel(
192                &mut self.buffer,
193                &self.translate,
194                &self.clip,
195                (self.width, self.height),
196                xy.x + x as isize,
197                xy.y + y,
198                *pixel,
199            );
200            x += 1;
201            if x >= image.width() {
202                x = 0;
203                y += 1;
204            }
205        }
206    }
207
208    /// Draw an indexed image at `x`, `y`
209    pub fn draw_indexed_image<P: Into<Coord>>(&mut self, xy: P, image: &IndexedImage) {
210        let xy = xy.into();
211        let palette = image.get_palette();
212        let (width, height) = image.size();
213        for x in 0..width {
214            for y in 0..height {
215                let i = image.get_pixel_index(x, y).unwrap();
216                let color_idx = image.get_pixel(i).unwrap() as usize;
217                let color = palette[color_idx];
218                update_pixel(
219                    &mut self.buffer,
220                    &self.translate,
221                    &self.clip,
222                    (self.width, self.height),
223                    x as isize + xy.x,
224                    y as isize + xy.y,
225                    color,
226                );
227            }
228        }
229    }
230
231    pub fn draw_wrapped_image<P: Into<Coord>>(&mut self, xy: P, image: &IndexedWrapper) {
232        match image {
233            IndexedWrapper::Static(img) => self.draw_indexed_image(xy, img),
234            IndexedWrapper::Animated(img) => self.draw_animated_image(xy, img),
235        }
236    }
237
238    /// Draw an animated image at `x`, `y`
239    pub fn draw_animated_image<P: Into<Coord>>(&mut self, xy: P, image: &AnimatedIndexedImage) {
240        let xy = xy.into();
241        let palette = image.get_palette();
242        let (width, height) = image.size();
243        let current_frame = image.get_current_frame_pixels();
244        for x in 0..width {
245            for y in 0..height {
246                let i = image.get_pixel_index(x, y).unwrap();
247                let color_idx = current_frame[i] as usize;
248                let color = palette[color_idx];
249                update_pixel(
250                    &mut self.buffer,
251                    &self.translate,
252                    &self.clip,
253                    (self.width, self.height),
254                    x as isize + xy.x,
255                    y as isize + xy.y,
256                    color,
257                );
258            }
259        }
260    }
261
262    pub fn draw_arc(
263        &mut self,
264        center: Coord,
265        angle_start: isize,
266        angle_end: isize,
267        radius: usize,
268        close: bool,
269        color: Color,
270    ) {
271        for r in angle_start..=angle_end {
272            let px = Coord::from_angle(center, radius, r);
273            update_pixel(
274                &mut self.buffer,
275                &self.translate,
276                &self.clip,
277                (self.width, self.height),
278                px.x,
279                px.y,
280                color,
281            );
282        }
283        if close {
284            self.draw_line(
285                center,
286                Coord::from_angle(center, radius, angle_start),
287                color,
288            );
289            self.draw_line(center, Coord::from_angle(center, radius, angle_end), color);
290        }
291    }
292
293    pub fn draw_line<P1: Into<Coord>, P2: Into<Coord>>(
294        &mut self,
295        start: P1,
296        end: P2,
297        color: Color,
298    ) {
299        let mut start = start.into();
300        let mut end = end.into();
301        if start.x > end.x || start.y > end.y {
302            swap(&mut start, &mut end);
303        }
304        if start.x == end.x {
305            for y in start.y..=end.y {
306                update_pixel(
307                    &mut self.buffer,
308                    &self.translate,
309                    &self.clip,
310                    (self.width, self.height),
311                    start.x,
312                    y,
313                    color,
314                );
315            }
316        } else if start.y == end.y {
317            for x in start.x..=end.x {
318                update_pixel(
319                    &mut self.buffer,
320                    &self.translate,
321                    &self.clip,
322                    (self.width, self.height),
323                    x,
324                    start.y,
325                    color,
326                );
327            }
328        } else {
329            let mut delta = 0;
330            let x1 = start.x;
331            let y1 = start.y;
332            let x2 = end.x;
333            let y2 = end.y;
334            let dx = isize::abs(x2 - x1);
335            let dy = isize::abs(y2 - y1);
336            let dx2 = dx * 2;
337            let dy2 = dy * 2;
338            let ix: isize = if x1 < x2 { 1 } else { -1 };
339            let iy: isize = if y1 < y2 { 1 } else { -1 };
340            let mut x = x1;
341            let mut y = y1;
342            if dx >= dy {
343                loop {
344                    update_pixel(
345                        &mut self.buffer,
346                        &self.translate,
347                        &self.clip,
348                        (self.width, self.height),
349                        x,
350                        y,
351                        color,
352                    );
353                    if x == x2 {
354                        break;
355                    }
356                    x += ix;
357                    delta += dy2;
358                    if delta > dx {
359                        y += iy;
360                        delta -= dx2;
361                    }
362                }
363            } else {
364                loop {
365                    update_pixel(
366                        &mut self.buffer,
367                        &self.translate,
368                        &self.clip,
369                        (self.width, self.height),
370                        x,
371                        y,
372                        color,
373                    );
374                    if y == y2 {
375                        break;
376                    }
377                    y += iy;
378                    delta += dx2;
379                    if delta > dy {
380                        x += ix;
381                        delta -= dy2;
382                    }
383                }
384            }
385        }
386    }
387
388    /// Draw renderable offset by [xy]
389    #[inline(always)]
390    pub fn draw_offset<T, P: Into<Coord>>(&mut self, xy: P, renderable: &dyn Renderable<T>) {
391        self.with_translate(xy.into(), |g| renderable.render(g));
392    }
393
394    /// Draw renderable
395    #[inline(always)]
396    pub fn draw<T>(&mut self, renderable: &dyn Renderable<T>) {
397        renderable.render(self);
398    }
399
400    /// Get the RGB values for a pixel
401    /// Alpha will always be 255
402    ///
403    /// If `use_translate` is true then the x,y will be updated with `self.translate`
404    ///
405    /// # Returns
406    ///
407    /// `Some(Color)` if x,y is within bounds
408    #[inline]
409    pub fn get_pixel(&self, x: isize, y: isize, use_translate: bool) -> Option<Color> {
410        let (x, y) = if use_translate {
411            (x + self.translate.x, y + self.translate.y)
412        } else {
413            (x, y)
414        };
415
416        if x >= 0 && y >= 0 && x < self.width as isize && y < self.height as isize {
417            let idx = self.index(x as usize, y as usize);
418            return Some(self.buffer.get_color(idx));
419        }
420
421        None
422    }
423
424    /// Set every pixel to `color`, this ignores translate and clip
425    #[inline(always)]
426    pub fn clear(&mut self, color: Color) {
427        (self.clear_method)(&mut self.buffer, color);
428    }
429
430    /// Set/blend every pixel with `color`, same as [clear] but this follows translate and clip
431    pub fn clear_aware(&mut self, color: Color) {
432        for y in 0..self.height {
433            for x in 0..self.width {
434                self.set_pixel(x as isize, y as isize, color);
435            }
436        }
437    }
438
439    /// Draw `chr` at `pos`
440    ///
441    /// # Usage
442    /// ```
443    ///# use buffer_graphics_lib::prelude::*;
444    ///# fn doc(graphics: &mut Graphics) {
445    ///     graphics.draw_letter((20,20), 'A', PixelFont::Limited3x5, BLUE);
446    ///# }
447    /// ```
448    #[inline(always)]
449    pub fn draw_letter(&mut self, pos: (isize, isize), chr: char, font: PixelFont, color: Color) {
450        self.draw_ascii_letter(pos, chr_to_code(chr), font, color);
451    }
452
453    /// Shouldn't be called in normal usage
454    pub fn draw_ascii_letter(
455        &mut self,
456        pos: (isize, isize),
457        code: u8,
458        font: PixelFont,
459        color: Color,
460    ) {
461        if code == 32 || code == 9 {
462            return;
463        }
464        let (width, height) = font.size();
465
466        let px: &[bool] = if let Some(custom) = self.custom_font.get(&code) {
467            match font {
468                PixelFont::Standard4x4 => &custom.font_4x4,
469                PixelFont::Script8x8 => &custom.font_8x8,
470                PixelFont::Outline7x9 => &custom.font_7x9,
471                PixelFont::Standard4x5 => &custom.font_4x5,
472                PixelFont::Standard6x7 => &custom.font_6x7,
473                PixelFont::Standard8x10 => &custom.font_8x10,
474                PixelFont::Limited3x5 => &custom.font_3x5,
475            }
476        } else {
477            font.pixels(code)
478        };
479
480        for x in 0..width {
481            for y in 0..height {
482                let i = x + y * width;
483                if px[i] {
484                    update_pixel(
485                        &mut self.buffer,
486                        &self.translate,
487                        &self.clip,
488                        (self.width, self.height),
489                        x as isize + pos.0,
490                        y as isize + pos.1,
491                        color,
492                    );
493                }
494            }
495        }
496    }
497
498    /// Should only be used by Text::render
499    /// `text` param must already be corrected wrapped
500    pub fn draw_ascii<P: Into<TextPos>, F: Into<TextFormat>>(
501        &mut self,
502        text: &[Vec<u8>],
503        pos: P,
504        format: F,
505    ) {
506        let format = format.into();
507        let font = format.font();
508        let color = format.color();
509        let per_x = format.char_width();
510        let per_y = format.line_height();
511
512        if per_y == 0 || per_x == 0 {
513            return;
514        }
515
516        let (start_x, start_y) = format.positioning().calc(
517            pos.into().to_coord(font),
518            text.iter().map(|list| list.len()).max().unwrap() * per_x.unsigned_abs(),
519            text.len() * per_y.unsigned_abs(),
520        );
521
522        for (y, line) in text.iter().enumerate() {
523            let y = y as isize * per_y;
524            for (x, char) in line.iter().enumerate() {
525                let x = x as isize * per_x;
526                self.draw_ascii_letter((start_x + x, start_y + y), *char, font, color);
527            }
528        }
529    }
530
531    /// Draw `text` on screen at `pos` using `format`
532    ///
533    /// # Params
534    /// * `format` See [TextFormat], can be
535    ///     * `TextFormat`
536    ///     * `Color`
537    ///     * `(Color, PixelFont)`
538    ///     * `(Color, PixelFont, Positioning)`
539    ///     * `(Color, PixelFont, WrappingStrategy, Positioning)`
540    ///     * `(Color, PixelFont, WrappingStrategy)`
541    ///     * `(Color, PixelFont, WrappingStrategy, f32)` (f32 = line height)
542    ///     * `(Color, PixelFont, WrappingStrategy, f32, f32)` (1st f32 = line height, 2nd f32 = char width)
543    ///     * `(Color, PixelFont, WrappingStrategy, f32, f32, Positioning)` (1st f32 = line height, 2nd f32 = char width)
544    ///
545    /// # Usage
546    ///
547    /// ```
548    ///# use buffer_graphics_lib::prelude::*;
549    ///# fn doc(graphics: &mut Graphics) {
550    ///  //simple example
551    ///  graphics.draw_text("Test", TextPos::ColRow(1,1), RED);
552    ///# }
553    ///
554    /// //full example
555    /// fn draw_message(graphics: &mut Graphics, msg: String) {
556    ///     let width_in_columns = PixelFont::Standard6x7.get_max_characters(graphics.width(), graphics.height()).0;
557    ///     graphics.draw_text(&msg, TextPos::px(coord!(8,8)), (BLACK, PixelFont::Standard6x7, WrappingStrategy::AtCol(width_in_columns - 1)));
558    /// }
559    /// ```
560    #[inline]
561    pub fn draw_text<P: Into<TextPos>, F: Into<TextFormat>>(
562        &mut self,
563        text: &str,
564        pos: P,
565        format: F,
566    ) {
567        let text = Text::new(text, pos.into(), format.into());
568        text.render(self);
569    }
570
571    #[inline]
572    pub fn draw_rect<R: Into<Rect>>(&mut self, rect: R, draw_type: DrawType) {
573        Drawable::from_obj(rect.into(), draw_type).render(self)
574    }
575
576    #[inline]
577    pub fn draw_circle<C: Into<Circle>>(&mut self, circle: C, draw_type: DrawType) {
578        Drawable::from_obj(circle.into(), draw_type).render(self)
579    }
580
581    #[inline]
582    pub fn draw_polygon<P: Into<Polygon>>(&mut self, polygon: P, draw_type: DrawType) {
583        Drawable::from_obj(polygon.into(), draw_type).render(self)
584    }
585
586    #[inline]
587    pub fn draw_triangle<T: Into<Triangle>>(&mut self, triangle: T, draw_type: DrawType) {
588        Drawable::from_obj(triangle.into(), draw_type).render(self)
589    }
590
591    #[inline]
592    pub fn draw_ellipse<E: Into<Ellipse>>(&mut self, ellipse: E, draw_type: DrawType) {
593        Drawable::from_obj(ellipse.into(), draw_type).render(self)
594    }
595
596    /// Update a pixel color, replacing or blending depending on whether `color`s alpha is 255 or not
597    ///
598    /// If the alpha is 0 the call does nothing
599    #[inline]
600    pub fn set_pixel(&mut self, x: isize, y: isize, color: Color) {
601        update_pixel(
602            &mut self.buffer,
603            &self.translate,
604            &self.clip,
605            (self.width, self.height),
606            x,
607            y,
608            color,
609        );
610    }
611}
612
613/// Update a pixel color, using [set_pixel] or [blend_pixel] depending on whether `color`s alpha is 255 or not
614///
615/// If the alpha is 0 the call is does nothing
616fn update_pixel(
617    buffer: &mut GraphicsBuffer,
618    translate: &Coord,
619    clip: &Clip,
620    (width, height): (usize, usize),
621    x: isize,
622    y: isize,
623    color: Color,
624) {
625    let x = x + translate.x;
626    let y = y + translate.y;
627    match buffer {
628        RgbaU8(buffer) => {
629            let idx = ((x + y * width as isize) * 4) as usize;
630            if x >= 0
631                && y >= 0
632                && x < width as isize
633                && y < height as isize
634                && clip.is_valid((x, y))
635            {
636                match color.a {
637                    255 => set_pixel_u8_rgba(buffer, idx, color),
638                    0 => {}
639                    _ => blend_pixel_u8_rgba(buffer, idx, color),
640                }
641            }
642        }
643        GraphicsBuffer::RgbaU32(buffer) => {
644            let idx = (x + y * width as isize) as usize;
645            if x >= 0
646                && y >= 0
647                && x < width as isize
648                && y < height as isize
649                && clip.is_valid((x, y))
650            {
651                match color.a {
652                    255 => set_pixel_32(buffer, idx, color, Color::to_rgba),
653                    0 => {}
654                    _ => blend_pixel_32(buffer, idx, color, Color::from_rgba, Color::to_rgba),
655                }
656            }
657        }
658        GraphicsBuffer::ArgbU32(buffer) => {
659            let idx = (x + y * width as isize) as usize;
660            if x >= 0
661                && y >= 0
662                && x < width as isize
663                && y < height as isize
664                && clip.is_valid((x, y))
665            {
666                match color.a {
667                    255 => set_pixel_32(buffer, idx, color, Color::to_argb),
668                    0 => {}
669                    _ => blend_pixel_32(buffer, idx, color, Color::from_argb, Color::to_argb),
670                }
671            }
672        }
673    }
674}
675
676/// Set the RGB values for a pixel
677///
678/// Generally you should use [update_pixel] instead
679///
680/// This ignores alpha, so 255,0,0,0 will draw a red pixel
681fn set_pixel_32(buffer: &mut [u32], idx: usize, color: Color, conv: fn(Color) -> u32) {
682    if idx < buffer.len() {
683        buffer[idx] = conv(color);
684    }
685}
686
687/// Set the RGB values for a pixel by blending it with the provided color
688/// This method uses alpha blending
689/// Generally you should use [update_pixel] instead
690fn blend_pixel_32(
691    buffer: &mut [u32],
692    idx: usize,
693    color: Color,
694    conv2: fn(u32) -> Color,
695    conv: fn(Color) -> u32,
696) {
697    let existing_color = conv2(buffer[idx]);
698    let new_color = existing_color.blend(color);
699    buffer[idx] = conv(new_color);
700}
701
702/// Set the RGB values for a pixel
703///
704/// Generally you should use [update_pixel] instead
705///
706/// This ignores alpha, so 255,0,0,0 will draw a red pixel
707fn set_pixel_u8_rgba(buffer: &mut [u8], idx: usize, color: Color) {
708    if idx < buffer.len() {
709        buffer[idx] = color.r;
710        buffer[idx + 1] = color.g;
711        buffer[idx + 2] = color.b;
712        buffer[idx + 3] = color.a;
713    }
714}
715
716/// Set the RGB values for a pixel by blending it with the provided color
717/// This method uses alpha blending
718/// Generally you should use [update_pixel] instead
719fn blend_pixel_u8_rgba(buffer: &mut [u8], idx: usize, color: Color) {
720    let existing_color = Color {
721        r: buffer[idx],
722        g: buffer[idx + 1],
723        b: buffer[idx + 2],
724        a: buffer[idx + 3],
725    };
726    let new_color = existing_color.blend(color);
727    buffer[idx] = new_color.r;
728    buffer[idx + 1] = new_color.g;
729    buffer[idx + 2] = new_color.b;
730    buffer[idx + 3] = new_color.a;
731}
732
733#[cfg(test)]
734mod test {
735    use super::*;
736    use crate::prelude::*;
737    use crate::shapes::polyline::Segment::*;
738    use crate::text::pos::TextPos::Px;
739
740    #[test]
741    fn is_inside() {
742        let mut buf = [0; 400];
743        let mut graphics = Graphics::new_u8_rgba(&mut buf, 10, 10).unwrap();
744        assert!(graphics.is_on_screen(Coord { x: 1, y: 1 }));
745        assert!(graphics.is_on_screen(Coord { x: 9, y: 9 }));
746        assert!(graphics.is_on_screen(Coord { x: 0, y: 0 }));
747        assert!(!graphics.is_on_screen(Coord { x: 10, y: 10 }));
748        assert!(!graphics.is_on_screen(Coord { x: 4, y: -1 }));
749        assert!(!graphics.is_on_screen(Coord { x: -1, y: 4 }));
750
751        graphics.set_translate(Coord { x: 2, y: -1 });
752        assert!(graphics.is_on_screen(Coord { x: 4, y: 4 }));
753        assert!(graphics.is_on_screen(Coord { x: 4, y: 0 }));
754        assert!(!graphics.is_on_screen(Coord { x: 0, y: 0 }));
755        assert!(!graphics.is_on_screen(Coord { x: 4, y: 9 }));
756    }
757
758    #[test]
759    fn check_draw() {
760        let mut buf = [0; 400];
761        let mut graphics = Graphics::new_u8_rgba(&mut buf, 10, 10).unwrap();
762
763        let drawable = Drawable::from_obj(Line::new((10, 10), (20, 20)), stroke(RED));
764        let text = Text::new("", Px(1, 1), WHITE);
765        let polyline = Polyline::new(
766            vec![Start(Coord::new(0, 0)), LineTo(Coord::new(0, 0))],
767            WHITE,
768        );
769
770        graphics.draw(&drawable);
771        graphics.draw(&text);
772        graphics.draw(&polyline);
773    }
774
775    #[test]
776    fn get_pixel_u8_all_rows() {
777        // Bug: get_pixel compares a byte-offset index against a pixel count (width*height),
778        // so pixels beyond roughly the first quarter of the buffer incorrectly return None.
779        let mut buf = Graphics::create_buffer_u8(10, 10);
780        let mut graphics = Graphics::new_u8_rgba(&mut buf, 10, 10).unwrap();
781        graphics.set_pixel(0, 0, RED);
782        graphics.set_pixel(0, 3, GREEN); // byte idx 120, len 100 → returns None with the bug
783        graphics.set_pixel(9, 9, BLUE);
784
785        assert_eq!(graphics.get_pixel(0, 0, false), Some(RED));
786        assert_eq!(graphics.get_pixel(0, 3, false), Some(GREEN));
787        assert_eq!(graphics.get_pixel(9, 9, false), Some(BLUE));
788        // Out-of-bounds should still return None
789        assert_eq!(graphics.get_pixel(10, 0, false), None);
790        assert_eq!(graphics.get_pixel(0, 10, false), None);
791    }
792}