ascii_forge/
window.rs

1use std::{
2    io::{self, Stdout, Write},
3    panic::{set_hook, take_hook},
4    time::Duration,
5};
6
7use crossterm::{
8    cursor::{self, Hide, Show},
9    event, execute, queue,
10    terminal::{self, *},
11    tty::IsTty,
12};
13
14pub use crate::prelude::*;
15
16#[derive(Default)]
17pub struct Inline {
18    active: bool,
19    kitty: bool,
20    start: u16,
21}
22
23impl AsMut<Buffer> for Window {
24    fn as_mut(&mut self) -> &mut Buffer {
25        self.buffer_mut()
26    }
27}
28
29/// The main window behind the application.
30/// Represents the terminal window, allowing it to be used similar to a buffer,
31/// but has extra event handling.
32/**
33```rust,
34use ascii_forge::prelude::*;
35
36let mut window = Window::init()?;
37
38render!(
39    window,
40    [
41        (10, 10) => "Element Here!"
42    ]
43)
44```
45*/
46pub struct Window {
47    io: io::Stdout,
48    buffers: [Buffer; 2],
49    active_buffer: usize,
50    events: Vec<Event>,
51
52    // Input Helpers,
53    mouse_pos: Vec2,
54
55    // Inlining
56    inline: Option<Inline>,
57
58    // Event Handling
59    just_resized: bool,
60}
61
62impl Default for Window {
63    fn default() -> Self {
64        Self::init().expect("Init should have succeeded")
65    }
66}
67
68impl Window {
69    /// Creates a new window from the given stdout.
70    /// Please prefer to use init as it will do all of the terminal init stuff.
71    pub fn new(io: io::Stdout) -> io::Result<Self> {
72        Ok(Self {
73            io,
74            buffers: [Buffer::new(size()?), Buffer::new(size()?)],
75            active_buffer: 0,
76            events: vec![],
77
78            mouse_pos: vec2(0, 0),
79
80            inline: None,
81
82            just_resized: false,
83        })
84    }
85
86    /// Creates a new window built for inline using the given Stdout and height.
87    pub fn new_inline(io: io::Stdout, height: u16) -> io::Result<Self> {
88        let size = vec2(size()?.0, height);
89        Ok(Self {
90            io,
91            buffers: [Buffer::new(size), Buffer::new(size)],
92            active_buffer: 0,
93            events: vec![],
94
95            mouse_pos: vec2(0, 0),
96
97            inline: Some(Inline::default()),
98
99            just_resized: false,
100        })
101    }
102
103    /// Initializes a window that is prepared for inline rendering.
104    /// Height is the number of columns that your terminal will need.
105    pub fn init_inline(height: u16) -> io::Result<Self> {
106        let stdout = io::stdout();
107
108        assert!(stdout.is_tty());
109
110        Window::new_inline(stdout, height)
111    }
112
113    /// Initializes the window, and returns a new Window for your use.
114    pub fn init() -> io::Result<Self> {
115        enable_raw_mode()?;
116
117        let mut stdout = io::stdout();
118
119        assert!(stdout.is_tty());
120
121        execute!(
122            stdout,
123            EnterAlternateScreen,
124            EnableMouseCapture,
125            EnableFocusChange,
126            Hide,
127            DisableLineWrap,
128        )?;
129
130        Window::new(stdout)
131    }
132
133    /// Enables the kitty keyboard protocol
134    pub fn keyboard(&mut self) -> io::Result<()> {
135        if let Ok(t) = terminal::supports_keyboard_enhancement() {
136            if !t {
137                return Err(io::Error::new(
138                    io::ErrorKind::Unsupported,
139                    "Terminal doesn't support the kitty keyboard protocol",
140                ));
141            }
142            if let Some(inline) = &mut self.inline {
143                inline.kitty = true;
144            } else {
145                execute!(
146                    self.io(),
147                    PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::all())
148                )?;
149            }
150            Ok(())
151        } else {
152            Err(io::Error::new(
153                io::ErrorKind::Unsupported,
154                "Terminal doesn't support the kitty keyboard protocol",
155            ))
156        }
157    }
158
159    /// Returns the active Buffer, as a reference.
160    pub fn buffer(&self) -> &Buffer {
161        &self.buffers[self.active_buffer]
162    }
163
164    /// Returns the active Buffer, as a mutable reference.
165    pub fn buffer_mut(&mut self) -> &mut Buffer {
166        &mut self.buffers[self.active_buffer]
167    }
168
169    /// Swaps the buffers, clearing the old buffer. Used automatically by the window's update method.
170    pub fn swap_buffers(&mut self) {
171        self.active_buffer = 1 - self.active_buffer;
172        self.buffers[self.active_buffer].clear();
173    }
174
175    /// Returns the current known size of the buffer's window.
176    pub fn size(&self) -> Vec2 {
177        self.buffer().size()
178    }
179
180    /// Restores the window to it's previous state from before the window's init method.
181    /// If the window is inline, restore the inline render
182    pub fn restore(&mut self) -> io::Result<()> {
183        if terminal::supports_keyboard_enhancement().is_ok() {
184            queue!(self.io, PopKeyboardEnhancementFlags)?;
185        }
186        if let Some(inline) = &self.inline {
187            execute!(
188                self.io,
189                DisableMouseCapture,
190                DisableFocusChange,
191                PopKeyboardEnhancementFlags,
192                Show,
193            )?;
194
195            if terminal::size()?.1 != inline.start + 1 {
196                print!(
197                    "{}",
198                    "\n".repeat(self.buffers[self.active_buffer].size().y as usize)
199                );
200            }
201
202            disable_raw_mode()?;
203
204            Ok(())
205        } else {
206            execute!(
207                self.io,
208                PopKeyboardEnhancementFlags,
209                LeaveAlternateScreen,
210                DisableMouseCapture,
211                DisableFocusChange,
212                Show,
213                EnableLineWrap,
214            )?;
215
216            disable_raw_mode()
217        }
218    }
219
220    /// Renders the window to the screen. should really only be used by the update method, but if you need a custom system, you can use this.
221    pub fn render(&mut self) -> io::Result<()> {
222        if self.inline.is_some() {
223            if !self.inline.as_ref().expect("Inline should be some").active {
224                // Make room for the inline render
225                print!("{}", "\n".repeat(self.buffer().size().y as usize));
226
227                enable_raw_mode()?;
228
229                execute!(
230                    self.io,
231                    EnableMouseCapture,
232                    EnableFocusChange,
233                    DisableLineWrap,
234                    Hide,
235                )?;
236
237                if self.inline.as_ref().expect("Inline should be some").kitty {
238                    execute!(
239                        self.io,
240                        PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::all())
241                    )?;
242                }
243
244                let inline = self.inline.as_mut().expect("Inline should be some");
245
246                inline.active = true;
247                inline.start = cursor::position()?.1;
248            }
249
250            for (loc, cell) in
251                self.buffers[1 - self.active_buffer].diff(&self.buffers[self.active_buffer])
252            {
253                queue!(
254                    self.io,
255                    cursor::MoveTo(
256                        loc.x,
257                        self.inline.as_ref().expect("Inline should be some").start
258                            - self.buffers[self.active_buffer].size().y
259                            + loc.y
260                    ),
261                    Print(cell),
262                )?;
263            }
264
265            queue!(
266                self.io,
267                cursor::MoveTo(
268                    0,
269                    self.inline.as_ref().expect("Inline should be some").start
270                        - self.buffers[self.active_buffer].size().y
271                )
272            )?;
273        } else {
274            if self.just_resized {
275                self.just_resized = false;
276                let cell = self.buffers[self.active_buffer].size();
277                for x in 0..cell.x {
278                    for y in 0..cell.y {
279                        let cell = self.buffers[self.active_buffer].get((x, y));
280                        queue!(self.io, cursor::MoveTo(x, y), Print(cell))?;
281                    }
282                }
283            }
284            for (loc, cell) in
285                self.buffers[1 - self.active_buffer].diff(&self.buffers[self.active_buffer])
286            {
287                queue!(self.io, cursor::MoveTo(loc.x, loc.y), Print(cell))?;
288            }
289        }
290        Ok(())
291    }
292
293    /// Handles events, and renders the screen.
294    pub fn update(&mut self, poll: Duration) -> io::Result<()> {
295        let cursor_pos = cursor::position()?;
296
297        // Render Window
298        self.render()?;
299
300        self.swap_buffers();
301
302        queue!(self.io, cursor::MoveTo(cursor_pos.0, cursor_pos.1))?;
303
304        // Flush Render To Stdout
305        self.io.flush()?;
306
307        // Poll For Events
308        self.handle_event(poll)?;
309
310        Ok(())
311    }
312
313    /// Handles events. Used automatically by the update method, so no need to use it unless update is being used.
314    pub fn handle_event(&mut self, poll: Duration) -> io::Result<()> {
315        self.events = vec![];
316
317        if event::poll(poll)? {
318            // Get all queued events
319            while event::poll(Duration::ZERO)? {
320                let event = event::read()?;
321
322                match event {
323                    Event::Resize(width, height) => {
324                        if self.inline.is_none() {
325                            self.buffers =
326                                [Buffer::new((width, height)), Buffer::new((width, height))];
327                            self.just_resized = true;
328                        }
329                    }
330                    Event::Mouse(MouseEvent { column, row, .. }) => {
331                        self.mouse_pos = vec2(column, row)
332                    }
333                    _ => {}
334                }
335
336                self.events.push(event);
337            }
338        }
339
340        Ok(())
341    }
342
343    pub fn mouse_pos(&self) -> Vec2 {
344        self.mouse_pos
345    }
346
347    /// Returns the current event for the frame, as a reference.
348    pub fn events(&self) -> &Vec<Event> {
349        &self.events
350    }
351
352    /// Returns true if the mouse cursor is hovering the given rect.
353    pub fn hover<V: Into<Vec2>>(&self, loc: V, size: V) -> io::Result<bool> {
354        let loc = loc.into();
355        let size = size.into();
356
357        let pos: Vec2 = self.mouse_pos();
358
359        Ok(pos.x <= loc.x + size.x && pos.x >= loc.x && pos.y <= loc.y + size.y && pos.y >= loc.y)
360    }
361
362    pub fn io(&mut self) -> &mut Stdout {
363        &mut self.io
364    }
365}
366
367/// A macro that allows you to quickly check an event based off of a pattern
368/// Takes in the window, a pattern for the if let statement, and finally a closure.
369/// This closure could be anything that returns a bool.
370///
371/// Underneath, the event! macro runs an if let on your pattern checking for any of the
372/// Events to be true from your given closure.
373/**
374Example
375```rust, no_run
376event!(window, Event::Key(e) => e.code == KeyCode::Char('q'));
377```
378*/
379#[macro_export]
380macro_rules! event {
381    ($window:expr, $event_type:pat => $($closure:tt)*) => {
382        $window.events().iter().any(|e| {
383            if let $event_type = e {
384                $($closure)*
385            } else {
386                false
387            }
388        })
389    };
390}
391
392/// Enables a panic hook to help you terminal still look pretty.
393pub fn handle_panics() {
394    let original_hook = take_hook();
395    set_hook(Box::new(move |e| {
396        Window::new(io::stdout())
397            .expect("Window should have created for panic")
398            .restore()
399            .expect("Window should have exited for panic");
400        original_hook(e);
401    }))
402}
403
404impl Drop for Window {
405    fn drop(&mut self) {
406        self.restore().expect("Restoration should have succeded");
407    }
408}