pixel_canvas/
canvas.rs

1//! The [`Canvas`] is the main entry point of the library. It handles window
2//! creation and input, calls your render callback, and presents the image on
3//! the screen.
4//!
5//! You create and configure a [`Canvas`] via builder methods. You can create
6//! a perfectly functionl, bare-bones canvas just by calling [`Canvas::new`]
7//! with your dimensions, and then calling [`render`]. If you
8//! want a fancier canvas (like handling input, with a custom title, etc.) you
9//! can configure that as well. For example:
10//! ```rust
11//! # use pixel_canvas::{Canvas, input::MouseState};
12//! let canvas = Canvas::new(512, 512)
13//!     .title("Tile")
14//!     .hidpi(true)
15//!     .show_ms(true)
16//!     .state(MouseState::new())
17//!     .input(MouseState::handle_input);
18//! ```
19//! This adds a 512x512 window called "Title", that renders in hidpi mode,
20//! displays the frame rendering time, and tracks the position of the mouse in
21//! physical pixels. For more information on event handlers, see the [`input`]
22//! module.
23//!
24//! [`Canvas`]: struct.Canvas.html
25//! [`Canvas::new`]: struct.Canvas.html#method.new
26//! [`render`]: struct.Canvas.html#method.render
27//! [`input`]: ../input/index.html
28//!
29//! Once you've created your canvas, you can use it to render your art. Do
30//! whatever you want in the render callback, the image you build will be
31//! displayed in the window when your render callback returns.
32//! ```rust,no_run
33//! # use pixel_canvas::{Canvas, Color, input::MouseState};
34//! # fn make_your_own_color(x: usize, y: usize, mx: i32, my: i32) -> Color {
35//! #     Color { r: 0, g: 0, b: 0 }
36//! # }
37//! # let canvas = Canvas::new(512, 512).state(MouseState::new());
38//! canvas.render(|mouse, image| {
39//!     let width = image.width() as usize;
40//!     for (y, row) in image.chunks_mut(width).enumerate() {
41//!         for (x, pixel) in row.iter_mut().enumerate() {
42//!             *pixel = make_your_own_color(x, y, mouse.x, mouse.y);
43//!         }
44//!     }
45//! });
46//! ```
47
48use crate::image::Image;
49use glium::{
50    glutin::{
51        self,
52        event::{Event, StartCause},
53        event_loop::ControlFlow,
54    },
55    Rect, Surface,
56};
57use std::time::{Duration, Instant};
58
59/// A type that represents an event handler.
60///
61/// It returns true if the state is changed.
62pub type EventHandler<State> = fn(&CanvasInfo, &mut State, &Event<()>) -> bool;
63
64/// Information about the [`Canvas`](struct.Canvas.html).
65pub struct CanvasInfo {
66    /// The width of the canvas, in virtual pixels.
67    pub width: usize,
68    /// The height of the canvas, in virtual pixels.
69    pub height: usize,
70    /// The base title for the window.
71    pub title: String,
72    /// Whether the canvas will render in hidpi mode. Defaults to `false`.
73    pub hidpi: bool,
74    /// The DPI factor. If hidpi is on, the virtual dimensions are multiplied
75    /// by this factor to create the actual image resolution. For example, if
76    /// you're on a Retina Macbook, this will be 2.0, so the image will be
77    /// twice the resolution that you specified.
78    pub dpi: f64,
79    /// Whether the window title will display the time to render a frame.
80    /// Defaults to `false`.
81    pub show_ms: bool,
82    /// Only call the render callback if there's a state change.
83    /// Defaults to `false`, which means it will instead render at a fixed framerate.
84    pub render_on_change: bool,
85}
86
87/// A [`Canvas`](struct.Canvas.html) manages a window and event loop, handing
88/// the current state to the renderer, and presenting its image on the screen.
89pub struct Canvas<State, Handler = EventHandler<State>> {
90    info: CanvasInfo,
91    image: Image,
92    state: State,
93    event_handler: Handler,
94}
95
96impl Canvas<()> {
97    /// Create a new canvas with a given virtual window dimensions.
98    pub fn new(width: usize, height: usize) -> Canvas<()> {
99        Canvas {
100            info: CanvasInfo {
101                width,
102                height,
103                hidpi: false,
104                dpi: 1.0,
105                title: "Canvas".into(),
106                show_ms: false,
107                render_on_change: false,
108            },
109            image: Image::new(width, height),
110            state: (),
111            event_handler: |_, (), _| false,
112        }
113    }
114}
115
116impl<State, Handler> Canvas<State, Handler>
117where
118    Handler: FnMut(&CanvasInfo, &mut State, &Event<()>) -> bool + 'static,
119    State: 'static,
120{
121    /// Set the attached state.
122    ///
123    /// Attaching a new state object will reset the input handler.
124    pub fn state<NewState>(self, state: NewState) -> Canvas<NewState, EventHandler<NewState>> {
125        Canvas {
126            info: self.info,
127            image: self.image,
128            state,
129            event_handler: |_, _, _| false,
130        }
131    }
132
133    /// Set the title on the canvas window.
134    pub fn title(self, text: impl Into<String>) -> Self {
135        Self {
136            info: CanvasInfo {
137                title: text.into(),
138                ..self.info
139            },
140            ..self
141        }
142    }
143
144    /// Toggle hidpi render.
145    ///
146    /// Defaults to `false`.
147    /// If you have a hidpi monitor, this will cause the image to be larger
148    /// than the dimensions you specified when creating the canvas.
149    pub fn hidpi(self, enabled: bool) -> Self {
150        Self {
151            info: CanvasInfo {
152                hidpi: enabled,
153                ..self.info
154            },
155            ..self
156        }
157    }
158
159    /// Whether to show a frame duration in the title bar.
160    ///
161    /// Defaults to `false`.
162    pub fn show_ms(self, enabled: bool) -> Self {
163        Self {
164            info: CanvasInfo {
165                show_ms: enabled,
166                ..self.info
167            },
168            ..self
169        }
170    }
171
172    /// Whether to render a new frame only on state changes.
173    ///
174    /// Defaults to `false`, which means it will render at a fixed framerate.
175    pub fn render_on_change(self, enabled: bool) -> Self {
176        Self {
177            info: CanvasInfo {
178                render_on_change: enabled,
179                ..self.info
180            },
181            ..self
182        }
183    }
184
185    /// Attach an input handler.
186    ///
187    /// Your input handler must be compatible with any state that you've set
188    /// previously. Your event handler will be called for each event with the
189    /// canvas information, the current state, and the inciting event.
190    pub fn input<NewHandler>(self, callback: NewHandler) -> Canvas<State, NewHandler>
191    where
192        NewHandler: FnMut(&CanvasInfo, &mut State, &Event<()>) -> bool + 'static,
193    {
194        Canvas {
195            info: self.info,
196            image: self.image,
197            state: self.state,
198            event_handler: callback,
199        }
200    }
201
202    /// Provide a rendering callback.
203    ///
204    /// The canvas will call your rendering callback on demant, with the
205    /// current state and a reference to the image. Depending on settings,
206    /// this will either be called at 60fps, or only called when state changes.
207    /// See [`render_on_change`](struct.Canvas.html#method.render_on_change).
208    pub fn render(mut self, mut callback: impl FnMut(&mut State, &mut Image) + 'static) {
209        let event_loop = glutin::event_loop::EventLoop::new();
210        let wb = glutin::window::WindowBuilder::new()
211            .with_title(&self.info.title)
212            .with_inner_size(glutin::dpi::LogicalSize::new(
213                self.info.width as f64,
214                self.info.height as f64,
215            ))
216            .with_resizable(false);
217        let cb = glutin::ContextBuilder::new().with_vsync(true);
218        let display = glium::Display::new(wb, cb, &event_loop).unwrap();
219
220        self.info.dpi = if self.info.hidpi {
221            display.gl_window().window().scale_factor()
222        } else {
223            1.0
224        };
225
226        let width = (self.info.width as f64 * self.info.dpi) as usize;
227        let height = (self.info.height as f64 * self.info.dpi) as usize;
228        self.image = Image::new(width, height);
229
230        let mut texture = glium::Texture2d::empty_with_format(
231            &display,
232            glium::texture::UncompressedFloatFormat::U8U8U8,
233            glium::texture::MipmapsOption::NoMipmap,
234            width as u32,
235            height as u32,
236        )
237        .unwrap();
238
239        let mut next_frame_time = Instant::now();
240        let mut should_render = true;
241        event_loop.run(move |event, _, control_flow| match event {
242            Event::NewEvents(StartCause::ResumeTimeReached { .. })
243            | Event::NewEvents(StartCause::Init) => {
244                next_frame_time = next_frame_time + Duration::from_nanos(16_666_667);
245                *control_flow = ControlFlow::WaitUntil(next_frame_time);
246                if !should_render {
247                    return;
248                }
249                if self.info.render_on_change {
250                    should_render = false;
251                }
252                let frame_start = Instant::now();
253
254                callback(&mut self.state, &mut self.image);
255                let width = self.image.width() as u32;
256                let height = self.image.height() as u32;
257                if width != texture.width() || height != texture.height() {
258                    texture = glium::Texture2d::empty_with_format(
259                        &display,
260                        glium::texture::UncompressedFloatFormat::U8U8U8,
261                        glium::texture::MipmapsOption::NoMipmap,
262                        width,
263                        height,
264                    )
265                    .unwrap();
266                    display
267                        .gl_window()
268                        .window()
269                        .set_inner_size(glutin::dpi::LogicalSize::new(width as f64, height as f64));
270                }
271                texture.write(
272                    Rect {
273                        left: 0,
274                        bottom: 0,
275                        width: width as u32,
276                        height: height as u32,
277                    },
278                    &self.image,
279                );
280
281                let target = display.draw();
282                texture
283                    .as_surface()
284                    .fill(&target, glium::uniforms::MagnifySamplerFilter::Linear);
285                target.finish().unwrap();
286
287                let frame_end = Instant::now();
288                if self.info.show_ms {
289                    display.gl_window().window().set_title(&format!(
290                        "{} - {:3}ms",
291                        self.info.title,
292                        frame_end.duration_since(frame_start).as_millis()
293                    ));
294                }
295            }
296            glutin::event::Event::WindowEvent {
297                event: glutin::event::WindowEvent::CloseRequested,
298                ..
299            } => {
300                *control_flow = ControlFlow::Exit;
301            }
302            event => {
303                let changed = (self.event_handler)(&self.info, &mut self.state, &event);
304                should_render = changed || !self.info.render_on_change;
305            }
306        })
307    }
308}