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}