pronto_graphics/
window.rs

1use std::{collections::VecDeque, process::exit};
2
3use sfml::{
4    graphics::{
5        CircleShape, Font as SfmlFont, PrimitiveType, RectangleShape,
6        RenderTarget, RenderWindow, Shape, Text, Texture as SfmlTexture,
7        Transformable, Vertex, VertexArray,
8    },
9    system::Clock,
10    window::{mouse::Button, Event, Key, Style, VideoMode},
11};
12
13use crate::{
14    color::Color,
15    font::{
16        default_font, font_store, font_store_add, init_default_font,
17        init_font_store, Font,
18    },
19    input::InputState,
20    render_parameters::RenderParameterState,
21    shape::{RenderTask, ShapeStore, Shapes},
22    texture::{init_texture_store, texture_store, texture_store_add, Texture},
23};
24
25/// The core type of the Pronto Graphics library.
26/// All drawing and keyboard/mouse interaction happens through an instance of [`Window`].
27/// It has to be updated every frame  with [`Window::update`] for drawings to be rendered and the keyboard/mouse state to be updated.
28///
29/// # Examples
30/// ```
31/// let mut pg = Window::new(800, 600, "Window Title"); // Create window
32/// loop {
33///     pg.circle((200,200), 50); // Draw to it
34///     pg.update(); // Update for drawing to appear
35/// }
36/// ```
37pub struct Window<'a> {
38    window: RenderWindow,
39    input_state: InputState,
40    render_queue: VecDeque<RenderTask>,
41    background_color: Color,
42    font: Option<Font>,
43    render_parameter_state: RenderParameterState,
44    shape_store: ShapeStore<'a>,
45    deltatime_clock: Clock,
46    deltatime: f32,
47    runtime_clock: Clock,
48    runtime: f32,
49}
50
51impl Window<'_> {
52    fn new_from_renderwindow(window: RenderWindow) -> Self {
53        init_texture_store();
54        init_default_font();
55        init_font_store();
56
57        let mut circle_shape = CircleShape::new(0., 32);
58        circle_shape.set_outline_thickness(1.);
59        let mut rectangle_shape = RectangleShape::new();
60        rectangle_shape.set_outline_thickness(1.);
61
62        let text = default_font()
63            .and_then(|default_font| Some(Text::new("", &default_font, 16)));
64
65        Self {
66            window,
67            input_state: InputState::new(),
68            render_queue: VecDeque::new(),
69            background_color: Color::LIGHT_GRAY,
70            font: None,
71            render_parameter_state: Default::default(),
72            shape_store: ShapeStore {
73                circle: circle_shape,
74                rectangle: rectangle_shape,
75                texture: RectangleShape::new(),
76                text: text,
77            },
78            runtime_clock: Clock::start(),
79            deltatime_clock: Clock::start(),
80            deltatime: 1. / 60., // So that we don't get problems in the first frame
81            runtime: 0.,
82        }
83    }
84
85    /// Create a new window of size (`width`, `height`) and with title `name`.
86    /// Can be directly drawn to with functions like [`Window::circle`]
87    /// and has to be updated with [`Window::update`].
88    pub fn new(width: u32, height: u32, name: &str) -> Self {
89        let mut window = RenderWindow::new(
90            (width, height),
91            name,
92            Style::TITLEBAR | Style::CLOSE,
93            &Default::default(),
94        );
95        window.set_vertical_sync_enabled(true);
96        window.set_key_repeat_enabled(false);
97
98        Self::new_from_renderwindow(window)
99    }
100
101    /// Create a new fullscreen window.
102    /// Can be directly drawn to with functions like [`Window::circle`]
103    /// and has to be updated with [`Window::update`].
104    pub fn new_fullscreen() -> Self {
105        let mut window = RenderWindow::new(
106            VideoMode::desktop_mode(),
107            "",
108            Style::FULLSCREEN,
109            &Default::default(),
110        );
111        window.set_vertical_sync_enabled(true);
112        window.set_key_repeat_enabled(false);
113
114        Self::new_from_renderwindow(window)
115    }
116
117    /// Has to be called every frame for drawings to appear on the screen and keyboard/mouse to be updated.
118    /// Note that this function will block for vertical sync.
119    pub fn update(&mut self) {
120        self.update_events();
121        self.update_draw();
122
123        self.deltatime = self.deltatime_clock.restart().as_seconds();
124        self.runtime = self.runtime_clock.elapsed_time().as_seconds();
125
126        self.render_parameter_state = Default::default();
127    }
128
129    fn update_events(&mut self) {
130        self.input_state.clear();
131        while let Some(event) = self.window.poll_event() {
132            self.input_state.handle_event(event);
133            match event {
134                Event::Closed
135                | Event::KeyPressed {
136                    code: Key::ESCAPE, ..
137                } => exit(0),
138                _ => {}
139            }
140        }
141    }
142
143    fn update_draw(&mut self) {
144        self.window.clear(self.background_color.into());
145        for task in &self.render_queue {
146            let RenderTask {
147                pos,
148                shape,
149                render_parameter_state: color_state,
150            } = task;
151
152            match shape {
153                Shapes::Circle { radius } => {
154                    let s = &mut self.shape_store.circle;
155                    s.set_radius(*radius);
156                    s.set_origin((s.radius(), s.radius()));
157                    s.set_position(*pos);
158                    s.set_fill_color(color_state.fill_color.into());
159                    s.set_outline_color(color_state.outline_color.into());
160                    self.window.draw(s);
161                }
162                Shapes::Rectangle { width, height } => {
163                    let s = &mut self.shape_store.rectangle;
164                    s.set_size((*width, *height));
165                    // s.set_origin((*width / 2., *height / 2.));
166                    s.set_position(*pos);
167                    s.set_fill_color(color_state.fill_color.into());
168                    s.set_outline_color(color_state.outline_color.into());
169                    self.window.draw(s);
170                }
171                Shapes::Lines { coords } => {
172                    let mut va =
173                        VertexArray::new(PrimitiveType::LINES, coords.len());
174                    for (i, v) in coords.iter().enumerate() {
175                        va[i] = Vertex::with_pos_color(
176                            (*v).into(),
177                            color_state.line_color.into(),
178                        );
179                    }
180
181                    self.window.draw(&va);
182                }
183                Shapes::Texture {
184                    texture,
185                    width,
186                    height,
187                } => {
188                    if let Some(tex) = &texture_store(*texture) {
189                        let s = &mut self.shape_store.texture;
190                        s.set_texture(tex, false);
191                        s.set_size((*width, *height));
192                        // s.set_origin((*width / 2., *height / 2.));
193                        s.set_position(*pos);
194                        self.window.draw(s);
195                    }
196                }
197                Shapes::Text { string, font } => {
198                    if let Some(t) = &mut self.shape_store.text {
199                        let sfml_font = font
200                            .and_then(|font| font_store(font)) // Get custom font from font store
201                            .or(default_font()); // Or use the default font
202                        if let Some(sfml_font) = sfml_font {
203                            // If some kind of font was found, set it
204                            t.set_font(sfml_font)
205                        }
206                        t.set_character_size(color_state.font_size);
207                        t.set_string(string);
208                        t.set_fill_color(color_state.font_color.into());
209                        t.set_position(*pos);
210                        self.window.draw(t);
211                    }
212                }
213            }
214        }
215        self.render_queue.clear();
216        self.window.display();
217    }
218
219    /// Set the background color of the window.
220    /// The background color does _not_ reset at the beginning of a new frame.
221    /// The initial value for the background color is [`Color::LIGHT_GRAY`].
222    pub fn background_color<C: Into<Color>>(&mut self, color: C) {
223        self.background_color = color.into();
224    }
225
226    /// Set the fill color for drawing shapes like [`Window::circle`].
227    /// The fill color is reset at the beginning of a new frame to a default value of [`Color::WHITE`].
228    pub fn fill_color<C: Into<Color>>(&mut self, color: C) {
229        self.render_parameter_state.fill_color = color.into();
230    }
231
232    /// Set the outline color for drawing shapes like [`Window::circle`].
233    /// The outline color is reset at the beginning of a new frame to a default value of [`Color::TRANSPARENT`].
234    pub fn outline_color<C: Into<Color>>(&mut self, color: C) {
235        self.render_parameter_state.outline_color = color.into();
236    }
237
238    /// Set the line color for drawing lines with [`Window::line`].
239    /// The line color is reset at the beginning of a new frame to a default value of [`Color::BLACK`].
240    pub fn line_color<C: Into<Color>>(&mut self, color: C) {
241        self.render_parameter_state.line_color = color.into();
242    }
243
244    /// Set the line color for drawing text with [`Window::text`].
245    /// The font color is reset at the beginning of a new frame to a default value of [`Color::BLACK`].
246    pub fn font_color<C: Into<Color>>(&mut self, color: C) {
247        self.render_parameter_state.font_color = color.into();
248    }
249
250    /// Set the font size for drawing text with [`Window::text`].
251    /// The font size is reset at the beginning of a new frame to a default value of `16`.
252    pub fn font_size(&mut self, size: u32) {
253        self.render_parameter_state.font_size = size;
254    }
255
256    /// Set the font for drawing text with [`Window::text`].
257    /// The font does _not_ reset at the beginning of a new frame.
258    /// Fonts can be loaded with [`Window::load_font`].
259    /// Initially or if a value of `None` is passed to this function,
260    /// a default font built into the library is used (Processing Sans Pro).
261    ///
262    /// # Examples
263    /// ```
264    /// let mut pg = Window::new_fullscreen();
265    /// let my_font = pg.load_font("MyFont.ttf").unwrap();
266    /// pg.font(Some(my_font));
267    /// loop {
268    ///     pg.text((20, 20), "This text is drawn in MyFont.");
269    ///
270    ///     pg.update();
271    /// }
272    /// ```
273    pub fn font(&mut self, font: Option<Font>) {
274        self.font = font
275    }
276
277    /// Draw a circle at position `pos` with radius `radius`.
278    /// The origin of the circle is at it's center.
279    pub fn circle(&mut self, pos: (f32, f32), radius: f32) {
280        self.render_queue.push_back(RenderTask {
281            pos,
282            shape: Shapes::Circle { radius },
283            render_parameter_state: self.render_parameter_state,
284        })
285    }
286
287    /// Draw a rectangle at position `pos` with width and height of `(width, height)`.
288    /// The origin of the rectangle is at it's top left.
289    pub fn rectangle(&mut self, pos: (f32, f32), width: f32, height: f32) {
290        self.render_queue.push_back(RenderTask {
291            pos,
292            shape: Shapes::Rectangle { width, height },
293            render_parameter_state: self.render_parameter_state,
294        })
295    }
296
297    /// Draw a square at position `pos` with a width and height of `size`.
298    /// The origin of the square is at it's top left.
299    pub fn square(&mut self, pos: (f32, f32), size: f32) {
300        self.render_queue.push_back(RenderTask {
301            pos,
302            shape: Shapes::Rectangle {
303                width: size,
304                height: size,
305            },
306            render_parameter_state: self.render_parameter_state,
307        })
308    }
309
310    /// Draw a texture `texture` at position `pos` with width and height of `(width, height)`.
311    /// The origin of the texture is at it's top left.
312    /// Textures can be loaded with [`Window::load_texture`].
313    /// # Examples
314    /// ```
315    /// let mut pg = Window::new_fullscreen();
316    /// let my_texture = pg.load_texture("my_texture.png").unwrap();
317    /// loop {
318    ///     pg.texture((100., 250.), my_texture, 100., 150.);
319    ///
320    ///     pg.update();
321    /// }
322    /// ```
323    pub fn texture(
324        &mut self,
325        pos: (f32, f32),
326        texture: Texture,
327        width: f32,
328        height: f32,
329    ) {
330        self.render_queue.push_back(RenderTask {
331            pos,
332            shape: Shapes::Texture {
333                texture,
334                width,
335                height,
336            },
337            render_parameter_state: self.render_parameter_state,
338        })
339    }
340
341    /// Draw a texture `texture` at position `pos` with width of `width`,
342    /// and height according to the aspect ratio of the texture.
343    /// The origin of the texture is at it's top left.
344    /// Textures can be loaded with [`Window::load_texture`].
345    /// # Examples
346    /// ```
347    /// let mut pg = Window::new_fullscreen();
348    /// let my_texture = pg.load_texture("my_texture.png").unwrap();
349    /// loop {
350    ///     pg.texture_((100., 250.), my_texture, 100.);
351    ///
352    ///     pg.update();
353    /// }
354    /// ```
355    pub fn texture_(&mut self, pos: (f32, f32), texture: Texture, width: f32) {
356        self.render_queue.push_back(RenderTask {
357            pos,
358            shape: Shapes::Texture {
359                texture,
360                width,
361                height: width / texture.aspect(),
362            },
363            render_parameter_state: self.render_parameter_state,
364        })
365    }
366
367    /// Draw text `string` at position `pos`.
368    /// The default font size is 16 and can be changed with [`Window::font_size`].
369    /// The default font color is [`Color::BLACK`] and can be changed with [`Window::font_color`].
370    /// Uses the default font built into the library (Processing Sans Pro)
371    /// or the font set with [`Window::font`].
372    /// # Examples
373    /// ```
374    /// let mut pg = Window::new(720, 480, "Window Title");
375    /// loop {
376    ///     pg.fill_color(Color::BLACK);
377    ///     pg.text((10., 10.), "Hello World!");
378    ///     pg.update();
379    /// }
380    /// ```
381    pub fn text(&mut self, pos: (f32, f32), string: &str) {
382        self.render_queue.push_back(RenderTask {
383            pos,
384            shape: Shapes::Text {
385                string: string.to_string(),
386                font: self.font,
387            },
388            render_parameter_state: self.render_parameter_state,
389        })
390    }
391
392    /// Draw a line from position `from` to position `to`.
393    /// The line's color is set with [`Window::line_color`].
394    pub fn line(&mut self, from: (f32, f32), to: (f32, f32)) {
395        match self.render_queue.back_mut() {
396            Some(RenderTask {
397                shape: Shapes::Lines { coords },
398                render_parameter_state: color_state,
399                ..
400            }) if color_state.line_color
401                == self.render_parameter_state.line_color =>
402            {
403                coords.push(from);
404                coords.push(to);
405            }
406            _ => {
407                self.render_queue.push_back(RenderTask {
408                    pos: (0., 0.),
409                    shape: Shapes::Lines {
410                        coords: vec![from, to],
411                    },
412                    render_parameter_state: self.render_parameter_state,
413                });
414            }
415        }
416    }
417
418    /// Whether the keyboard key `key` is currently held pressed.
419    /// # Examples
420    /// ```
421    /// if pg.key_pressed(Key::SPACE) {
422    ///     /*...*/
423    /// }
424    /// ```
425    pub fn key_pressed(&self, key: Key) -> bool {
426        self.input_state.key_pressed(key)
427    }
428
429    /// Whether the keyboard key `key` has just been pressed in this frame.
430    /// # Examples
431    /// ```
432    /// if pg.key_just_pressed(Key::SPACE) {
433    ///     /*...*/
434    /// }
435    /// ```
436    pub fn key_just_pressed(&self, key: Key) -> bool {
437        self.input_state.key_just_pressed(key)
438    }
439
440    /// Whether the mouse button `button` is currently held pressed.
441    /// # Examples
442    /// ```
443    /// if pg.mouse_pressed(Button::LEFT) {
444    ///     /*...*/
445    /// }
446    /// ```
447    pub fn mouse_pressed(&self, button: Button) -> bool {
448        self.input_state.mouse_pressed(button)
449    }
450
451    /// Whether the mouse button `button`  has just been pressed in this frame.
452    /// # Examples
453    /// ```
454    /// if pg.mouse_just_pressed(Button::LEFT) {
455    ///     /*...*/
456    /// }
457    /// ```
458    pub fn mouse_just_pressed(&self, button: Button) -> bool {
459        self.input_state.mouse_just_pressed(button)
460    }
461
462    /// The current mouse position inside the window.
463    pub fn mouse_position(&self) -> (f32, f32) {
464        self.input_state.mouse_position()
465    }
466
467    /// The current cumulative scroll wheel state of the mouse.
468    pub fn mouse_wheel(&self) -> f32 {
469        self.input_state.mouse_wheel()
470    }
471
472    /// How much the scroll wheel has been scrolled in this frame.
473    pub fn mouse_wheel_delta(&self) -> f32 {
474        self.input_state.mouse_wheel_delta()
475    }
476
477    /// The width of the window, or the width of the screen in fullscreen mode.
478    pub fn width(&self) -> f32 {
479        self.window.size().x as f32
480    }
481
482    /// The height of the window, or the height of the screen in fullscreen mode.
483    pub fn height(&self) -> f32 {
484        self.window.size().y as f32
485    }
486
487    /// The time since the window has been created in seconds.
488    pub fn time(&self) -> f32 {
489        self.runtime
490    }
491
492    /// How much time has passed since the last frame.
493    pub fn deltatime(&self) -> f32 {
494        self.deltatime
495    }
496
497    /// Load a texture from path `path`.
498    /// A return value of `None` means that the texture could not be loaded.
499    /// On success, returns a [`Texture`] object that can be passed to the [`Window::texture`] function to draw the texture to the screen.
500    ///
501    /// # Examples
502    /// ```
503    /// let mut pg = Window::new_fullscreen();
504    /// let my_texture = pg.load_texture("my_texture.png").unwrap();
505    /// loop {
506    ///     pg.texture_((100., 250.), my_texture, 100.);
507    ///
508    ///     pg.update();
509    /// }
510    /// ```
511    pub fn load_texture(&mut self, path: &str) -> Option<Texture> {
512        texture_store_add(SfmlTexture::from_file(path)?)
513    }
514
515    /// Load a font from path `path`.
516    /// A return value of `None` means that the font could not be loaded.
517    /// On success, returns a [`Font`] object that can be passed to the [`Window::font`] function
518    /// to set the font to be used for drawing text with [`Window::text`].
519    ///
520    /// # Examples
521    /// ```
522    /// let mut pg = Window::new_fullscreen();
523    /// let my_font = pg.load_font("MyFont.ttf").unwrap();
524    /// pg.font(Some(my_font));
525    /// loop {
526    ///     pg.text((20, 20), "This text is drawn in MyFont.");
527    ///
528    ///     pg.update();
529    /// }
530    /// ```
531    pub fn load_font(&mut self, path: &str) -> Option<Font> {
532        font_store_add(SfmlFont::from_file(path)?)
533    }
534}