apricity 0.1.0

Simple UI library for teaching purposes
Documentation
use std::time::Duration;

pub use rusttype::Font;

use sdl2::keyboard::Keycode;
use sdl2::pixels::Color;

pub use sdl2::event::Event;
pub use sdl2::rect::Rect;

use crate::Point;

pub struct SimpleImage {
    data: Vec<u8>,
    width: u32,
    height: u32,
}

impl SimpleImage {
    pub fn new(width: u32, height: u32) -> Self {
        let len = 4*width as usize*height as usize;
        SimpleImage {
            data: (0..len).map(|_| 0).collect(),
            width,
            height,
        }
    }

    pub fn create_text_image(
        font: &Font<'static>,
        text: &str,
        size: f32,
        color: [u8; 3],
    ) -> Result<SimpleImage, Box<dyn std::error::Error>> {
        let scale = rusttype::Scale::uniform(size);
        let point = rusttype::point(0.0, 0.0);
        let glyphs: Vec<_> = font.layout(text, scale, point).collect();
        let (y_min, y_max, width) = glyphs.iter()
            .filter_map(|glyph| glyph.pixel_bounding_box())
            .fold((0i32, 0i32, 0i32), |(y_min, y_max, width), bbox| {
                (
                    y_min.min(bbox.min.y),
                    y_max.max(bbox.max.y),
                    width.max(bbox.max.x)
                )
            });
        let height = y_max - y_min;
        let mut buffer = SimpleImage::new(width as u32, height as u32);

        for glyph in &glyphs {
            let bbox = match glyph.pixel_bounding_box() {
                Some(x) => x,
                None => continue,
            };

            glyph.draw(|x, y, w| {
                let x = x as i32 + bbox.min.x;
                let y = y as i32 + bbox.min.y - y_min;
                buffer[(x as u32, y as u32)] = [
                    color[2],
                    color[1],
                    color[0],
                    (255.0*w) as u8,
                ];
            });
        }

        Ok(buffer)
    }

    pub fn width(&self) -> u32 { self.width }
    pub fn height(&self) -> u32 { self.height }

    pub fn draw_polygon(&mut self, polygon: &[Point], value: [u8; 4])
    {
        let lines: Vec<(Point, Point)> = polygon.iter()
            .copied()
            .zip(polygon.iter().copied().cycle().skip(1))
            .collect();

        let mut top = f64::MAX;
        let mut bottom = 0.0f64;
        for (p0, p1) in lines.iter() {
            top = top.min(p0.y).min(p1.y);
            bottom = bottom.max(p0.y).max(p1.y);
        }

        let mut y = top;
        while y < bottom {
            let mut intersections = vec![];
            for (mut a, mut b) in lines.iter().copied() {
                if a.y == b.y {
                    continue;
                }

                if a.y > b.y {
                    (a, b) = (b, a);
                }
                if y < a.y || y > b.y {
                    continue;
                }

                if a.x == b.x {
                    intersections.push(a.x as i32);
                    continue;
                }

                let k = (b.y - a.y)/(b.x - a.x);
                let m = a.y - k*a.x;
                let x = (y as f64 - m)/k;
                intersections.push(x as i32);
            }

            intersections.sort();

            for (x0, x1) in intersections.iter().copied()
                .zip(intersections.iter().copied().skip(1))
                .enumerate()
                .filter_map(|(i, l)| if i % 2 == 0 { Some(l) } else { None }) {

                for x in x0..=x1 {
                    self[(x as u32, y as u32)] = value;
                }
            }

            y += 0.5;
        }

        for (p0, p1) in lines.iter() {
            let start = (p0.x as i32, p0.y as i32);
            let stop = (p1.x as i32, p1.y as i32);
            for (x, y) in line_drawing::Bresenham::new(start, stop) {
                self[(x as u32, y as u32)] = [0, 0, 0, 0xFF].into();
            }
        }
    }
}

impl std::ops::Deref for SimpleImage {
    type Target = [u8];

    fn deref(&self) -> &Self::Target {
        self.data.as_slice()
    }
}

impl std::ops::Index<(u32, u32)> for SimpleImage {
    type Output = [u8; 4];

    fn index(&self, (x, y): (u32, u32)) -> &Self::Output {
        let idx = 4*y*self.width + 4*x;
        self.data[idx as usize..idx as usize + 4].try_into().unwrap()
    }
}

impl std::ops::IndexMut<(u32, u32)> for SimpleImage {
    fn index_mut(&mut self, (x, y): (u32, u32)) -> &mut Self::Output {
        let idx = 4*y*self.width + 4*x;
        (&mut self.data[idx as usize..idx as usize + 4]).try_into().unwrap()
    }
}

pub struct SimpleWindow<S> {
    context: sdl2::Sdl,
    canvas: sdl2::render::Canvas<sdl2::video::Window>,
    width: u32,
    height: u32,
    state: S,
}

impl<S> SimpleWindow<S> {
    pub fn new(width: u32, height: u32, state: S) -> Result<SimpleWindow<S>, Box<dyn std::error::Error>> {
        let context = sdl2::init()?;
        let video_subsystem = context.video()?;

        let window = video_subsystem.window("rust-sdl2 demo", width, height)
            .position_centered()
            .build()?;

        let canvas = window.into_canvas().build()?;

        Ok(SimpleWindow {
            context,
            canvas,
            width,
            height,
            state,
        })
    }

    pub fn width(&self) -> u32 { self.width }
    pub fn height(&self) -> u32 { self.height }
    pub fn state(&self) -> &S { &self.state }
    pub fn state_mut(&mut self) -> &mut S { &mut self.state }

    pub fn draw_image(
        &mut self,
        image: &SimpleImage,
        target: Option<sdl2::rect::Rect>,
        blend: bool,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let texture_creator = self.canvas.texture_creator();

        let mut texture = texture_creator.create_texture_static(
            sdl2::pixels::PixelFormatEnum::ARGB8888,
            image.width(),
            image.height(),
        )?;
        if blend {
            texture.set_blend_mode(sdl2::render::BlendMode::Blend);
        }
        texture.update(None, &*image, 4*image.width() as usize)?;

        self.canvas.copy(
            &texture,
            None,
            target,
        )?;

        Ok(())
    }

    pub fn stroke_circle(
        &mut self,
        cx: f64,
        cy: f64,
        radius: f64,
        thickness: f64,
        color: [u8; 4],
    ) -> Result<(), Box<dyn std::error::Error>> {
        let x0 = (cx - radius).max(0.0);
        let y0 = (cy - radius).max(0.0);
        let x1 = (cx + radius).min(self.width as f64);
        let y1 = (cy + radius).min(self.height as f64);

        let r0 = radius - thickness;
        let r1 = radius;

        self.canvas.set_draw_color((color[0], color[1], color[2], color[3]));

        let mut x = x0;
        while x < x1 {
            let mut y = y0;
            while y < y1 {
                let r = ((x - cx).powi(2) + (y - cy).powi(2)).sqrt();
                if r < r0 || r > r1 {
                    y += 1.0;
                    continue;
                }

                self.canvas.draw_point((x as i32, y as i32))?;

                y += 1.0;
            }
            x += 1.0;
        }

        Ok(())
    }

    pub fn run<F>(mut self, callback: F) -> Result<(), Box<dyn std::error::Error>>
        where F: Fn(&mut SimpleWindow<S>, Vec<sdl2::event::Event>) -> Result<(), Box<dyn std::error::Error>>,
    {
        self.canvas.set_draw_color(Color::RGBA(0, 0, 0, 0xFF));
        self.canvas.clear();
        self.canvas.present();
        let mut event_pump = self.context.event_pump()?;
        'running: loop {
            self.canvas.clear();

            let mut events = vec![];
            for event in event_pump.poll_iter() {
                match event {
                    Event::Quit {..} |
                    Event::KeyDown { keycode: Some(Keycode::Escape), .. } => {
                        break 'running
                    },
                    _ => events.push(event),
                }
            }

            callback(&mut self, events)?;

            self.canvas.present();
            ::std::thread::sleep(Duration::new(0, 1_000_000_000u32 / 60));
        }

        Ok(())
    }
}