1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
//! The [`Canvas`] is the main entry point of the library. It handles window
//! creation and input, calls your render callback, and presents the image on
//! the screen.
//!
//! You create and configure a [`Canvas`] via builder methods. You can create
//! a perfectly functionl, bare-bones canvas just by calling [`Canvas::new`]
//! with your dimensions, and then calling [`render`]. If you
//! want a fancier canvas (like handling input, with a custom title, etc.) you
//! can configure that as well. For example:
//! ```rust
//! # use pixel_canvas::{Canvas, input::MouseState};
//! let canvas = Canvas::new(512, 512)
//!     .title("Tile")
//!     .hidpi(true)
//!     .show_ms(true)
//!     .state(MouseState::new())
//!     .input(MouseState::handle_input);
//! ```
//! This adds a 512x512 window called "Title", that renders in hidpi mode,
//! displays the frame rendering time, and tracks the position of the mouse in
//! physical pixels. For more information on event handlers, see the [`input`]
//! module.
//!
//! [`Canvas`]: struct.Canvas.html
//! [`Canvas::new`]: struct.Canvas.html#method.new
//! [`render`]: struct.Canvas.html#method.render
//! [`input`]: ../input/index.html
//!
//! Once you've created your canvas, you can use it to render your art. Do
//! whatever you want in the render callback, the image you build will be
//! displayed in the window when your render callback returns.
//! ```rust,no_run
//! # use pixel_canvas::{Canvas, Color, input::MouseState};
//! # fn make_your_own_color(x: usize, y: usize, mx: i32, my: i32) -> Color {
//! #     Color { r: 0, g: 0, b: 0 }
//! # }
//! # let canvas = Canvas::new(512, 512).state(MouseState::new());
//! canvas.render(|mouse, image| {
//!     let width = image.width() as usize;
//!     for (y, row) in image.chunks_mut(width).enumerate() {
//!         for (x, pixel) in row.iter_mut().enumerate() {
//!             *pixel = make_your_own_color(x, y, mouse.x, mouse.y);
//!         }
//!     }
//! });
//! ```

use crate::image::Image;
use glium::{
    glutin::{
        self,
        event::{Event, StartCause},
        event_loop::ControlFlow,
    },
    Rect, Surface,
};
use std::time::{Duration, Instant};

/// A type that represents an event handler.
///
/// It returns true if the state is changed.
pub type EventHandler<State> = fn(&CanvasInfo, &mut State, &Event<()>) -> bool;

/// Information about the [`Canvas`](struct.Canvas.html).
pub struct CanvasInfo {
    /// The width of the canvas, in virtual pixels.
    pub width: usize,
    /// The height of the canvas, in virtual pixels.
    pub height: usize,
    /// The base title for the window.
    pub title: String,
    /// Whether the canvas will render in hidpi mode. Defaults to `false`.
    pub hidpi: bool,
    /// The DPI factor. If hidpi is on, the virtual dimensions are multiplied
    /// by this factor to create the actual image resolution. For example, if
    /// you're on a Retina Macbook, this will be 2.0, so the image will be
    /// twice the resolution that you specified.
    pub dpi: f64,
    /// Whether the window title will display the time to render a frame.
    /// Defaults to `false`.
    pub show_ms: bool,
    /// Only call the render callback if there's a state change.
    /// Defaults to `false`, which means it will instead render at a fixed framerate.
    pub render_on_change: bool,
}

/// A [`Canvas`](struct.Canvas.html) manages a window and event loop, handing
/// the current state to the renderer, and presenting its image on the screen.
pub struct Canvas<State, Handler = EventHandler<State>> {
    info: CanvasInfo,
    image: Image,
    state: State,
    event_handler: Handler,
}

impl Canvas<()> {
    /// Create a new canvas with a given virtual window dimensions.
    pub fn new(width: usize, height: usize) -> Canvas<()> {
        Canvas {
            info: CanvasInfo {
                width,
                height,
                hidpi: false,
                dpi: 1.0,
                title: "Canvas".into(),
                show_ms: false,
                render_on_change: false,
            },
            image: Image::new(width, height),
            state: (),
            event_handler: |_, (), _| false,
        }
    }
}

impl<State, Handler> Canvas<State, Handler>
where
    Handler: FnMut(&CanvasInfo, &mut State, &Event<()>) -> bool + 'static,
    State: 'static,
{
    /// Set the attached state.
    ///
    /// Attaching a new state object will reset the input handler.
    pub fn state<NewState>(self, state: NewState) -> Canvas<NewState, EventHandler<NewState>> {
        Canvas {
            info: self.info,
            image: self.image,
            state,
            event_handler: |_, _, _| false,
        }
    }

    /// Set the title on the canvas window.
    pub fn title(self, text: impl Into<String>) -> Self {
        Self {
            info: CanvasInfo {
                title: text.into(),
                ..self.info
            },
            ..self
        }
    }

    /// Toggle hidpi render.
    ///
    /// Defaults to `false`.
    /// If you have a hidpi monitor, this will cause the image to be larger
    /// than the dimensions you specified when creating the canvas.
    pub fn hidpi(self, enabled: bool) -> Self {
        Self {
            info: CanvasInfo {
                hidpi: enabled,
                ..self.info
            },
            ..self
        }
    }

    /// Whether to show a frame duration in the title bar.
    ///
    /// Defaults to `false`.
    pub fn show_ms(self, enabled: bool) -> Self {
        Self {
            info: CanvasInfo {
                show_ms: enabled,
                ..self.info
            },
            ..self
        }
    }

    /// Whether to render a new frame only on state changes.
    ///
    /// Defaults to `false`, which means it will render at a fixed framerate.
    pub fn render_on_change(self, enabled: bool) -> Self {
        Self {
            info: CanvasInfo {
                render_on_change: enabled,
                ..self.info
            },
            ..self
        }
    }

    /// Attach an input handler.
    ///
    /// Your input handler must be compatible with any state that you've set
    /// previously. Your event handler will be called for each event with the
    /// canvas information, the current state, and the inciting event.
    pub fn input<NewHandler>(self, callback: NewHandler) -> Canvas<State, NewHandler>
    where
        NewHandler: FnMut(&CanvasInfo, &mut State, &Event<()>) -> bool + 'static,
    {
        Canvas {
            info: self.info,
            image: self.image,
            state: self.state,
            event_handler: callback,
        }
    }

    /// Provide a rendering callback.
    ///
    /// The canvas will call your rendering callback on demant, with the
    /// current state and a reference to the image. Depending on settings,
    /// this will either be called at 60fps, or only called when state changes.
    /// See [`render_on_change`](struct.Canvas.html#method.render_on_change).
    pub fn render(mut self, mut callback: impl FnMut(&mut State, &mut Image) + 'static) {
        let event_loop = glutin::event_loop::EventLoop::new();
        let wb = glutin::window::WindowBuilder::new()
            .with_title(&self.info.title)
            .with_inner_size(glutin::dpi::LogicalSize::new(
                self.info.width as f64,
                self.info.height as f64,
            ))
            .with_resizable(false);
        let cb = glutin::ContextBuilder::new().with_vsync(true);
        let display = glium::Display::new(wb, cb, &event_loop).unwrap();

        self.info.dpi = if self.info.hidpi {
            display.gl_window().window().scale_factor()
        } else {
            1.0
        };

        let width = (self.info.width as f64 * self.info.dpi) as usize;
        let height = (self.info.height as f64 * self.info.dpi) as usize;
        self.image = Image::new(width, height);

        let mut texture = glium::Texture2d::empty_with_format(
            &display,
            glium::texture::UncompressedFloatFormat::U8U8U8,
            glium::texture::MipmapsOption::NoMipmap,
            width as u32,
            height as u32,
        )
        .unwrap();

        let mut next_frame_time = Instant::now();
        let mut should_render = true;
        event_loop.run(move |event, _, control_flow| match event {
            Event::NewEvents(StartCause::ResumeTimeReached { .. })
            | Event::NewEvents(StartCause::Init) => {
                next_frame_time = next_frame_time + Duration::from_nanos(16_666_667);
                *control_flow = ControlFlow::WaitUntil(next_frame_time);
                if !should_render {
                    return;
                }
                if self.info.render_on_change {
                    should_render = false;
                }
                let frame_start = Instant::now();

                callback(&mut self.state, &mut self.image);
                let width = self.image.width() as u32;
                let height = self.image.height() as u32;
                if width != texture.width() || height != texture.height() {
                    texture = glium::Texture2d::empty_with_format(
                        &display,
                        glium::texture::UncompressedFloatFormat::U8U8U8,
                        glium::texture::MipmapsOption::NoMipmap,
                        width,
                        height,
                    )
                    .unwrap();
                    display
                        .gl_window()
                        .window()
                        .set_inner_size(glutin::dpi::LogicalSize::new(width as f64, height as f64));
                }
                texture.write(
                    Rect {
                        left: 0,
                        bottom: 0,
                        width: width as u32,
                        height: height as u32,
                    },
                    &self.image,
                );

                let target = display.draw();
                texture
                    .as_surface()
                    .fill(&target, glium::uniforms::MagnifySamplerFilter::Linear);
                target.finish().unwrap();

                let frame_end = Instant::now();
                if self.info.show_ms {
                    display.gl_window().window().set_title(&format!(
                        "{} - {:3}ms",
                        self.info.title,
                        frame_end.duration_since(frame_start).as_millis()
                    ));
                }
            }
            glutin::event::Event::WindowEvent {
                event: glutin::event::WindowEvent::CloseRequested,
                ..
            } => {
                *control_flow = ControlFlow::Exit;
            }
            event => {
                let changed = (self.event_handler)(&self.info, &mut self.state, &event);
                should_render = changed || !self.info.render_on_change;
            }
        })
    }
}