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