chargrid_ggez 0.5.0

Graphical chargrid context which renders with ggez
Documentation
use chargrid_input::{keys, Input, KeyboardInput, MouseButton, MouseInput, ScrollDirection};
use chargrid_runtime::{app, on_frame, on_input, Component, Coord, FrameBuffer, Rgba32, Size};
use ggez::winit;
use std::time::Instant;

const FONT_NAME_NORMAL: &'static str = "normal";
const FONT_NAME_BOLD: &'static str = "bold";

pub struct FontBytes {
    pub normal: Vec<u8>,
    pub bold: Vec<u8>,
}

#[derive(Clone, Copy, Debug)]
pub struct Dimensions<T> {
    pub width: T,
    pub height: T,
}

pub struct Config {
    pub title: String,
    pub font_bytes: FontBytes,
    pub window_dimensions_px: Dimensions<f64>,
    pub cell_dimensions_px: Dimensions<f64>,
    pub font_scale: Dimensions<f64>,
    pub underline_width_cell_ratio: f64,
    pub underline_top_offset_cell_ratio: f64,
    pub resizable: bool,
}

pub struct Context {
    config: Config,
}

struct Fonts {
    normal: ggez::graphics::FontData,
    bold: ggez::graphics::FontData,
}

struct GgezApp<C>
where
    C: 'static + Component<State = (), Output = app::Output>,
{
    chargrid_core: C,
    chargrid_frame_buffer: FrameBuffer,
    last_frame: Instant,
    font_scale: ggez::graphics::PxScale,
    underline_mesh: ggez::graphics::Mesh,
    background_mesh: ggez::graphics::Mesh,
    cell_width: f32,
    cell_height: f32,
    current_mouse_button: Option<MouseButton>,
    current_mouse_position: Coord,
    #[cfg(feature = "gamepad")]
    gamepad_id_to_integer_id: hashbrown::HashMap<ggez::event::GamepadId, u64>,
}

impl<C> GgezApp<C>
where
    C: 'static + Component<State = (), Output = app::Output>,
{
    fn convert_mouse_position(&self, x: f32, y: f32) -> Coord {
        Coord {
            x: (x / self.cell_width) as i32,
            y: (y / self.cell_height) as i32,
        }
    }

    fn convert_mouse_button(button: ggez::event::MouseButton) -> Option<MouseButton> {
        match button {
            ggez::input::mouse::MouseButton::Left => Some(MouseButton::Left),
            ggez::input::mouse::MouseButton::Right => Some(MouseButton::Right),
            ggez::input::mouse::MouseButton::Middle => Some(MouseButton::Middle),
            ggez::input::mouse::MouseButton::Other(_) => None,
        }
    }
}

impl<C> ggez::event::EventHandler<ggez::GameError> for GgezApp<C>
where
    C: 'static + Component<State = (), Output = app::Output>,
{
    fn update(&mut self, ctx: &mut ggez::Context) -> ggez::GameResult {
        const DESIRED_FPS: u32 = 60;
        while ctx.time.check_update_time(DESIRED_FPS) {
            let now = Instant::now();
            if let Some(app::Exit) = on_frame(
                &mut self.chargrid_core,
                now - self.last_frame,
                &mut self.chargrid_frame_buffer,
            ) {
                ctx.request_quit();
            }
            self.last_frame = now;
        }
        Ok(())
    }

    fn draw(&mut self, ctx: &mut ggez::Context) -> ggez::GameResult {
        let mut canvas =
            ggez::graphics::Canvas::from_frame(ctx, ggez::graphics::Color::from([0., 0., 0., 1.]));
        for (coord, cell) in self.chargrid_frame_buffer.enumerate() {
            if cell.character != ' ' {
                let mut text = ggez::graphics::Text::new(cell.character);
                let font = if cell.bold {
                    FONT_NAME_BOLD
                } else {
                    FONT_NAME_NORMAL
                };
                text.set_font(font);
                text.set_scale(self.font_scale);
                canvas.draw(
                    &text,
                    ggez::graphics::DrawParam::new()
                        .dest([
                            coord.x as f32 * self.cell_width,
                            coord.y as f32 * self.cell_height,
                        ])
                        .color(cell.foreground.to_f32_array_01())
                        .z(1),
                );
            }
            if cell.background != Rgba32::new(0, 0, 0, 255) {
                canvas.draw(
                    &self.background_mesh,
                    ggez::graphics::DrawParam::new()
                        .dest([
                            coord.x as f32 * self.cell_width,
                            coord.y as f32 * self.cell_height,
                        ])
                        .color(cell.background.to_f32_array_01()),
                );
            }
            if cell.underline {
                canvas.draw(
                    &self.underline_mesh,
                    ggez::graphics::DrawParam::default()
                        .dest(ggez::mint::Point2 {
                            x: coord.x as f32 * self.cell_width,
                            y: coord.y as f32 * self.cell_height,
                        })
                        .color(cell.foreground.to_f32_array_01()),
                );
            }
        }
        canvas.finish(ctx).unwrap();
        ggez::timer::yield_now();
        Ok(())
    }

    fn resize_event(
        &mut self,
        _ctx: &mut ggez::Context,
        _width: f32,
        _height: f32,
    ) -> Result<(), ggez::GameError> {
        Ok(())
    }

    fn key_down_event(
        &mut self,
        ctx: &mut ggez::Context,
        input: ggez::input::keyboard::KeyInput,
        _repeat: bool,
    ) -> Result<(), ggez::GameError> {
        if let Some(keycode) = input.keycode {
            use winit::event::VirtualKeyCode;
            let key_char_shift = |lower: char, upper: char| {
                KeyboardInput::Char(
                    if input.mods.contains(ggez::input::keyboard::KeyMods::SHIFT) {
                        upper
                    } else {
                        lower
                    },
                )
            };
            let keyboard_input = match keycode {
                VirtualKeyCode::A => key_char_shift('a', 'A'),
                VirtualKeyCode::B => key_char_shift('b', 'B'),
                VirtualKeyCode::C => key_char_shift('c', 'C'),
                VirtualKeyCode::D => key_char_shift('d', 'D'),
                VirtualKeyCode::E => key_char_shift('e', 'E'),
                VirtualKeyCode::F => key_char_shift('f', 'F'),
                VirtualKeyCode::G => key_char_shift('g', 'G'),
                VirtualKeyCode::H => key_char_shift('h', 'H'),
                VirtualKeyCode::I => key_char_shift('i', 'I'),
                VirtualKeyCode::J => key_char_shift('j', 'J'),
                VirtualKeyCode::K => key_char_shift('k', 'K'),
                VirtualKeyCode::L => key_char_shift('l', 'L'),
                VirtualKeyCode::M => key_char_shift('m', 'M'),
                VirtualKeyCode::N => key_char_shift('n', 'N'),
                VirtualKeyCode::O => key_char_shift('o', 'O'),
                VirtualKeyCode::P => key_char_shift('p', 'P'),
                VirtualKeyCode::Q => key_char_shift('q', 'Q'),
                VirtualKeyCode::R => key_char_shift('r', 'R'),
                VirtualKeyCode::S => key_char_shift('s', 'S'),
                VirtualKeyCode::T => key_char_shift('t', 'T'),
                VirtualKeyCode::U => key_char_shift('u', 'U'),
                VirtualKeyCode::V => key_char_shift('v', 'V'),
                VirtualKeyCode::W => key_char_shift('w', 'W'),
                VirtualKeyCode::X => key_char_shift('x', 'X'),
                VirtualKeyCode::Y => key_char_shift('y', 'Y'),
                VirtualKeyCode::Z => key_char_shift('z', 'Z'),
                VirtualKeyCode::Key1 => KeyboardInput::Char('1'),
                VirtualKeyCode::Key2 => KeyboardInput::Char('2'),
                VirtualKeyCode::Key3 => KeyboardInput::Char('3'),
                VirtualKeyCode::Key4 => KeyboardInput::Char('4'),
                VirtualKeyCode::Key5 => KeyboardInput::Char('5'),
                VirtualKeyCode::Key6 => KeyboardInput::Char('6'),
                VirtualKeyCode::Key7 => KeyboardInput::Char('7'),
                VirtualKeyCode::Key8 => KeyboardInput::Char('8'),
                VirtualKeyCode::Key9 => KeyboardInput::Char('9'),
                VirtualKeyCode::Key0 => KeyboardInput::Char('0'),
                VirtualKeyCode::Numpad1 => KeyboardInput::Char('1'),
                VirtualKeyCode::Numpad2 => KeyboardInput::Char('2'),
                VirtualKeyCode::Numpad3 => KeyboardInput::Char('3'),
                VirtualKeyCode::Numpad4 => KeyboardInput::Char('4'),
                VirtualKeyCode::Numpad5 => KeyboardInput::Char('5'),
                VirtualKeyCode::Numpad6 => KeyboardInput::Char('6'),
                VirtualKeyCode::Numpad7 => KeyboardInput::Char('7'),
                VirtualKeyCode::Numpad8 => KeyboardInput::Char('8'),
                VirtualKeyCode::Numpad9 => KeyboardInput::Char('9'),
                VirtualKeyCode::Numpad0 => KeyboardInput::Char('0'),
                VirtualKeyCode::F1 => KeyboardInput::Function(1),
                VirtualKeyCode::F2 => KeyboardInput::Function(2),
                VirtualKeyCode::F3 => KeyboardInput::Function(3),
                VirtualKeyCode::F4 => KeyboardInput::Function(4),
                VirtualKeyCode::F5 => KeyboardInput::Function(5),
                VirtualKeyCode::F6 => KeyboardInput::Function(6),
                VirtualKeyCode::F7 => KeyboardInput::Function(7),
                VirtualKeyCode::F8 => KeyboardInput::Function(8),
                VirtualKeyCode::F9 => KeyboardInput::Function(9),
                VirtualKeyCode::F10 => KeyboardInput::Function(10),
                VirtualKeyCode::F11 => KeyboardInput::Function(11),
                VirtualKeyCode::F12 => KeyboardInput::Function(12),
                VirtualKeyCode::F13 => KeyboardInput::Function(13),
                VirtualKeyCode::F14 => KeyboardInput::Function(14),
                VirtualKeyCode::F15 => KeyboardInput::Function(15),
                VirtualKeyCode::F16 => KeyboardInput::Function(16),
                VirtualKeyCode::F17 => KeyboardInput::Function(17),
                VirtualKeyCode::F18 => KeyboardInput::Function(18),
                VirtualKeyCode::F19 => KeyboardInput::Function(19),
                VirtualKeyCode::F20 => KeyboardInput::Function(20),
                VirtualKeyCode::F21 => KeyboardInput::Function(21),
                VirtualKeyCode::F22 => KeyboardInput::Function(22),
                VirtualKeyCode::F23 => KeyboardInput::Function(23),
                VirtualKeyCode::F24 => KeyboardInput::Function(24),
                VirtualKeyCode::At => KeyboardInput::Char('@'),
                VirtualKeyCode::Plus => KeyboardInput::Char('+'),
                VirtualKeyCode::Minus => KeyboardInput::Char('-'),
                VirtualKeyCode::Equals => key_char_shift('=', '+'),
                VirtualKeyCode::Backslash => KeyboardInput::Char('\\'),
                VirtualKeyCode::Grave => KeyboardInput::Char('`'),
                VirtualKeyCode::Apostrophe => KeyboardInput::Char('\''),
                VirtualKeyCode::LBracket => KeyboardInput::Char('['),
                VirtualKeyCode::RBracket => KeyboardInput::Char(']'),
                VirtualKeyCode::Period => KeyboardInput::Char('.'),
                VirtualKeyCode::Comma => KeyboardInput::Char(','),
                VirtualKeyCode::Slash => KeyboardInput::Char('/'),
                VirtualKeyCode::NumpadAdd => KeyboardInput::Char('+'),
                VirtualKeyCode::NumpadSubtract => KeyboardInput::Char('-'),
                VirtualKeyCode::NumpadMultiply => KeyboardInput::Char('*'),
                VirtualKeyCode::NumpadDivide => KeyboardInput::Char('/'),
                VirtualKeyCode::PageUp => KeyboardInput::PageUp,
                VirtualKeyCode::PageDown => KeyboardInput::PageDown,
                VirtualKeyCode::Home => KeyboardInput::Home,
                VirtualKeyCode::End => KeyboardInput::End,
                VirtualKeyCode::Up => KeyboardInput::Up,
                VirtualKeyCode::Down => KeyboardInput::Down,
                VirtualKeyCode::Left => KeyboardInput::Left,
                VirtualKeyCode::Right => KeyboardInput::Right,
                VirtualKeyCode::Return => keys::RETURN,
                VirtualKeyCode::Escape => keys::ESCAPE,
                VirtualKeyCode::Space => KeyboardInput::Char(' '),
                VirtualKeyCode::Back => keys::BACKSPACE,
                VirtualKeyCode::Delete => KeyboardInput::Delete,
                other => {
                    log::warn!("Unhandled input: {:?}", other);
                    return Ok(());
                }
            };
            if let Some(app::Exit) = on_input(
                &mut self.chargrid_core,
                Input::Keyboard(keyboard_input),
                &self.chargrid_frame_buffer,
            ) {
                ctx.request_quit();
            }
        }
        Ok(())
    }

    fn mouse_button_down_event(
        &mut self,
        ctx: &mut ggez::Context,
        button: ggez::event::MouseButton,
        x: f32,
        y: f32,
    ) -> Result<(), ggez::GameError> {
        if let Some(button) = Self::convert_mouse_button(button) {
            self.current_mouse_button = Some(button);
            let coord = self.convert_mouse_position(x, y);
            self.current_mouse_position = coord;
            let input = MouseInput::MousePress { button, coord };
            if let Some(app::Exit) = on_input(
                &mut self.chargrid_core,
                Input::Mouse(input),
                &self.chargrid_frame_buffer,
            ) {
                ctx.request_quit();
            }
        }
        Ok(())
    }

    fn mouse_button_up_event(
        &mut self,
        ctx: &mut ggez::Context,
        button: ggez::event::MouseButton,
        x: f32,
        y: f32,
    ) -> Result<(), ggez::GameError> {
        if let Some(button) = Self::convert_mouse_button(button) {
            self.current_mouse_button = None;
            let coord = self.convert_mouse_position(x, y);
            self.current_mouse_position = coord;
            let input = MouseInput::MouseRelease {
                button: Ok(button),
                coord,
            };
            if let Some(app::Exit) = on_input(
                &mut self.chargrid_core,
                Input::Mouse(input),
                &self.chargrid_frame_buffer,
            ) {
                ctx.request_quit();
            }
        }
        Ok(())
    }

    fn mouse_motion_event(
        &mut self,
        ctx: &mut ggez::Context,
        x: f32,
        y: f32,
        _dx: f32,
        _dy: f32,
    ) -> Result<(), ggez::GameError> {
        let coord = self.convert_mouse_position(x, y);
        self.current_mouse_position = coord;
        let input = MouseInput::MouseMove {
            coord,
            button: self.current_mouse_button,
        };
        if let Some(app::Exit) = on_input(
            &mut self.chargrid_core,
            Input::Mouse(input),
            &self.chargrid_frame_buffer,
        ) {
            ctx.request_quit();
        }
        Ok(())
    }

    fn mouse_wheel_event(
        &mut self,
        ctx: &mut ggez::Context,
        x: f32,
        y: f32,
    ) -> Result<(), ggez::GameError> {
        let mut handle = |direction| {
            let coord = self.current_mouse_position;
            let input = MouseInput::MouseScroll { direction, coord };
            if let Some(app::Exit) = on_input(
                &mut self.chargrid_core,
                Input::Mouse(input),
                &self.chargrid_frame_buffer,
            ) {
                ctx.request_quit();
            }
        };
        if x > 0.0 {
            handle(ScrollDirection::Right);
        }
        if x < 0.0 {
            handle(ScrollDirection::Left);
        }
        if y > 0.0 {
            handle(ScrollDirection::Up);
        }
        if y < 0.0 {
            handle(ScrollDirection::Down);
        }
        Ok(())
    }

    fn quit_event(&mut self, _ctx: &mut ggez::Context) -> Result<bool, ggez::GameError> {
        Ok(false)
    }

    #[cfg(feature = "gamepad")]
    fn gamepad_button_down_event(
        &mut self,
        ctx: &mut ggez::Context,
        btn: ggez::event::Button,
        id: ggez::event::GamepadId,
    ) -> Result<(), ggez::GameError> {
        use chargrid_input::{GamepadButton, GamepadInput};
        let num_gamepad_ids = self.gamepad_id_to_integer_id.len() as u64;
        let &mut integer_id = self
            .gamepad_id_to_integer_id
            .entry(id)
            .or_insert(num_gamepad_ids);
        let button = match btn {
            ggez::event::Button::DPadUp => GamepadButton::DPadUp,
            ggez::event::Button::DPadRight => GamepadButton::DPadRight,
            ggez::event::Button::DPadDown => GamepadButton::DPadDown,
            ggez::event::Button::DPadLeft => GamepadButton::DPadLeft,
            ggez::event::Button::North => GamepadButton::North,
            ggez::event::Button::East => GamepadButton::East,
            ggez::event::Button::South => GamepadButton::South,
            ggez::event::Button::West => GamepadButton::West,
            ggez::event::Button::Start => GamepadButton::Start,
            ggez::event::Button::Select => GamepadButton::Select,
            ggez::event::Button::LeftTrigger => GamepadButton::LeftBumper,
            ggez::event::Button::RightTrigger => GamepadButton::RightBumper,
            other => {
                log::warn!("Unhandled input: {:?}", other);
                return Ok(());
            }
        };
        let input = GamepadInput {
            button,
            id: integer_id,
        };
        if let Some(app::Exit) = on_input(
            &mut self.chargrid_core,
            Input::Gamepad(input),
            &self.chargrid_frame_buffer,
        ) {
            ctx.request_quit();
        }
        Ok(())
    }
}

impl Context {
    pub fn new(config: Config) -> Self {
        Self { config }
    }

    pub fn run<C>(self, component: C) -> !
    where
        C: 'static + Component<State = (), Output = app::Output>,
    {
        let Self { config } = self;
        let grid_size = Size::new(
            (config.window_dimensions_px.width as f64 / config.cell_dimensions_px.width) as u32,
            (config.window_dimensions_px.height as f64 / config.cell_dimensions_px.height) as u32,
        );
        let chargrid_frame_buffer = FrameBuffer::new(grid_size);
        let (mut ctx, events_loop) =
            ggez::ContextBuilder::new(config.title.as_str(), "chargrid_ggez")
                .window_setup(ggez::conf::WindowSetup::default().title(config.title.as_str()))
                .window_mode(
                    ggez::conf::WindowMode::default()
                        .dimensions(
                            config.window_dimensions_px.width as f32,
                            config.window_dimensions_px.height as f32,
                        )
                        .resizable(config.resizable),
                )
                .build()
                .expect("failed to initialize ggez");
        let fonts = Fonts {
            normal: ggez::graphics::FontData::from_vec(config.font_bytes.normal.to_vec())
                .expect("failed to load normal font"),
            bold: ggez::graphics::FontData::from_vec(config.font_bytes.bold.to_vec())
                .expect("failed to load bold font"),
        };
        let underline_mesh = {
            let underline_mid_cell_ratio =
                config.underline_top_offset_cell_ratio + config.underline_width_cell_ratio / 2.0;
            let underline_cell_position =
                (underline_mid_cell_ratio * config.cell_dimensions_px.height) as f32;
            let underline_width =
                (config.underline_width_cell_ratio * config.cell_dimensions_px.height) as f32;
            let points = [
                ggez::mint::Point2 {
                    x: 0.0,
                    y: underline_cell_position,
                },
                ggez::mint::Point2 {
                    x: config.cell_dimensions_px.width as f32,
                    y: underline_cell_position,
                },
            ];
            let mesh = ggez::graphics::Mesh::new_line(
                &mut ctx,
                &points,
                underline_width,
                [1., 1., 1., 1.].into(),
            )
            .expect("failed to build mesh for underline");
            mesh
        };
        let background_mesh = {
            let rect = ggez::graphics::Rect {
                x: 0.0,
                y: 0.0,
                w: config.cell_dimensions_px.width as f32,
                h: config.cell_dimensions_px.height as f32,
            };
            let mesh = ggez::graphics::Mesh::new_rectangle(
                &mut ctx,
                ggez::graphics::DrawMode::fill(),
                rect,
                [1., 1., 1., 1.].into(),
            )
            .expect("failed to build mesh for background");
            mesh
        };
        ctx.gfx.add_font(FONT_NAME_NORMAL, fonts.normal);
        ctx.gfx.add_font(FONT_NAME_BOLD, fonts.bold);
        ggez::event::run(
            ctx,
            events_loop,
            GgezApp {
                chargrid_core: component,
                chargrid_frame_buffer,
                last_frame: Instant::now(),
                font_scale: ggez::graphics::PxScale {
                    x: config.font_scale.width as f32,
                    y: config.font_scale.height as f32,
                },
                underline_mesh,
                background_mesh,
                cell_width: config.cell_dimensions_px.width as f32,
                cell_height: config.cell_dimensions_px.height as f32,
                current_mouse_button: None,
                current_mouse_position: Coord::new(0, 0),
                #[cfg(feature = "gamepad")]
                gamepad_id_to_integer_id: hashbrown::HashMap::default(),
            },
        )
    }
}