ascii_forge/
window.rs

1pub use crate::prelude::*;
2
3use crossterm::{
4    cursor::{self, Hide, MoveTo, Show},
5    event, execute, queue,
6    terminal::{self, *},
7    tty::IsTty,
8};
9use std::{
10    io::{self, Stdout, Write},
11    panic::{set_hook, take_hook},
12    time::Duration,
13};
14
15#[derive(Default)]
16pub struct Inline {
17    active: bool,
18    kitty: bool,
19    start: u16,
20}
21
22impl AsMut<Buffer> for Window {
23    fn as_mut(&mut self) -> &mut Buffer {
24        self.buffer_mut()
25    }
26}
27
28/// The main window behind the application.
29/// Represents the terminal window, allowing it to be used similar to a buffer,
30/// but has extra event handling.
31/**
32```rust, no_run
33# use ascii_forge::prelude::*;
34# fn main() -> std::io::Result<()> {
35let mut window = Window::init()?;
36render!(window, (10, 10) => [ "Element Here!" ]);
37# Ok(())
38# }
39```
40*/
41pub struct Window {
42    io: io::Stdout,
43    buffers: [Buffer; 2],
44    active_buffer: usize,
45    events: Vec<Event>,
46
47    last_cursor: (bool, Vec2, SetCursorStyle),
48
49    cursor_visible: bool,
50    cursor: Vec2,
51    cursor_style: SetCursorStyle,
52
53    // Input Helpers,
54    mouse_pos: Vec2,
55    // Inlining
56    inline: Option<Inline>,
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: [
74                Buffer::new_filled(size()?, ' '),
75                Buffer::new_filled(size()?, ' '),
76            ],
77            active_buffer: 0,
78            events: vec![],
79            last_cursor: (false, vec2(0, 0), SetCursorStyle::SteadyBlock),
80            cursor_visible: false,
81            cursor_style: SetCursorStyle::SteadyBlock,
82            cursor: vec2(0, 0),
83            mouse_pos: vec2(0, 0),
84            inline: None,
85            just_resized: false,
86        })
87    }
88
89    /// Creates a new window built for inline using the given Stdout and height.
90    pub fn new_inline(io: io::Stdout, height: u16) -> io::Result<Self> {
91        let size = vec2(size()?.0, height);
92        Ok(Self {
93            io,
94            buffers: [Buffer::new_filled(size, ' '), Buffer::new_filled(size, ' ')],
95            active_buffer: 0,
96            events: vec![],
97            last_cursor: (false, vec2(0, 0), SetCursorStyle::SteadyBlock),
98            cursor_visible: false,
99            cursor_style: SetCursorStyle::SteadyBlock,
100            cursor: vec2(0, 0),
101            mouse_pos: vec2(0, 0),
102            inline: Some(Inline::default()),
103            just_resized: false,
104        })
105    }
106
107    /// Initializes a window that is prepared for inline rendering.
108    /// Height is the number of columns that your terminal will need.
109    pub fn init_inline(height: u16) -> io::Result<Self> {
110        let stdout = io::stdout();
111        assert!(stdout.is_tty());
112        Window::new_inline(stdout, height)
113    }
114
115    /// Initializes the window, and returns a new Window for your use.
116    pub fn init() -> io::Result<Self> {
117        enable_raw_mode()?;
118        let mut stdout = io::stdout();
119        assert!(stdout.is_tty());
120        execute!(
121            stdout,
122            EnterAlternateScreen,
123            EnableMouseCapture,
124            EnableFocusChange,
125            EnableBracketedPaste,
126            Hide,
127            DisableLineWrap,
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].fill(' ');
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                DisableBracketedPaste,
191                PopKeyboardEnhancementFlags,
192                Show,
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            disable_raw_mode()?;
201            Ok(())
202        } else {
203            execute!(
204                self.io,
205                PopKeyboardEnhancementFlags,
206                LeaveAlternateScreen,
207                DisableMouseCapture,
208                DisableFocusChange,
209                DisableBracketedPaste,
210                Show,
211                EnableLineWrap,
212            )?;
213            disable_raw_mode()
214        }
215    }
216
217    /// 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.
218    pub fn render(&mut self) -> io::Result<()> {
219        if self.inline.is_some() {
220            if !self.inline.as_ref().expect("Inline should be some").active {
221                // Make room for the inline render
222                print!("{}", "\n".repeat(self.buffer().size().y as usize));
223
224                enable_raw_mode()?;
225
226                execute!(
227                    self.io,
228                    EnableMouseCapture,
229                    EnableFocusChange,
230                    EnableBracketedPaste,
231                    DisableLineWrap,
232                    Hide,
233                )?;
234                if self.inline.as_ref().expect("Inline should be some").kitty {
235                    execute!(
236                        self.io,
237                        PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::all())
238                    )?;
239                }
240                let inline = self.inline.as_mut().expect("Inline should be some");
241                inline.active = true;
242                inline.start = cursor::position()?.1;
243            }
244
245            for (loc, cell) in
246                self.buffers[1 - self.active_buffer].diff(&self.buffers[self.active_buffer])
247            {
248                queue!(
249                    self.io,
250                    cursor::MoveTo(
251                        loc.x,
252                        self.inline.as_ref().expect("Inline should be some").start
253                            - self.buffers[self.active_buffer].size().y
254                            + loc.y
255                    ),
256                    Print(cell),
257                )?;
258            }
259
260            queue!(
261                self.io,
262                cursor::MoveTo(
263                    0,
264                    self.inline.as_ref().expect("Inline should be some").start
265                        - self.buffers[self.active_buffer].size().y
266                )
267            )?;
268        } else {
269            if self.just_resized {
270                self.just_resized = false;
271                let cell = self.buffers[self.active_buffer].size();
272                for x in 0..cell.x {
273                    for y in 0..cell.y {
274                        let cell = self.buffers[self.active_buffer]
275                            .get((x, y))
276                            .expect("Cell should be in bounds");
277                        queue!(self.io, cursor::MoveTo(x, y), Print(cell))?;
278                    }
279                }
280            }
281
282            for (loc, cell) in
283                self.buffers[1 - self.active_buffer].diff(&self.buffers[self.active_buffer])
284            {
285                queue!(self.io, cursor::MoveTo(loc.x, loc.y), Print(cell))?;
286            }
287        }
288        Ok(())
289    }
290
291    /// Handles events, and renders the screen.
292    pub fn update(&mut self, poll: Duration) -> io::Result<()> {
293        // Render Window
294        self.render()?;
295        self.swap_buffers();
296        self.render_cursor()?;
297        // Flush Render To Stdout
298        self.io.flush()?;
299        // Poll For Events
300        self.handle_event(poll)?;
301        Ok(())
302    }
303
304    pub fn render_cursor(&mut self) -> io::Result<()> {
305        // Get the current cursor position
306        if self.cursor_style != self.last_cursor.2
307            || self.cursor != self.last_cursor.1
308            || self.cursor_visible != self.last_cursor.0
309        {
310            if self.cursor_visible {
311                let cursor = self.cursor;
312                let style = self.cursor_style;
313
314                // Calculate the actual position based on inline rendering
315                let actual_pos = if let Some(inline) = &self.inline {
316                    vec2(
317                        cursor.x,
318                        inline.start - self.buffers[self.active_buffer].size().y + cursor.y,
319                    )
320                } else {
321                    cursor
322                };
323
324                queue!(self.io(), MoveTo(actual_pos.x, actual_pos.y), style, Show)?;
325            } else {
326                queue!(self.io(), Hide)?;
327            }
328        }
329        self.last_cursor = (self.cursor_visible, self.cursor, self.cursor_style);
330        Ok(())
331    }
332
333    /// Handles events. Used automatically by the update method, so no need to use it unless update is being used.
334    pub fn handle_event(&mut self, poll: Duration) -> io::Result<()> {
335        self.events = vec![];
336        if event::poll(poll)? {
337            // Get all queued events
338            while event::poll(Duration::ZERO)? {
339                let event = event::read()?;
340                match event {
341                    Event::Resize(width, height) => {
342                        if self.inline.is_none() {
343                            self.buffers = [
344                                Buffer::new_filled((width, height), ' '),
345                                Buffer::new_filled((width, height), ' '),
346                            ];
347                            self.just_resized = true;
348                        }
349                    }
350                    Event::Mouse(MouseEvent { column, row, .. }) => {
351                        self.mouse_pos = vec2(column, row)
352                    }
353                    _ => {}
354                }
355                self.events.push(event);
356            }
357        }
358        Ok(())
359    }
360
361    /// Returns whether the cursor is visible
362    pub fn cursor_visible(&self) -> bool {
363        self.cursor_visible
364    }
365
366    /// Returns the current cursor position
367    pub fn cursor(&self) -> Vec2 {
368        self.cursor
369    }
370
371    /// Returns the current cursor style
372    pub fn cursor_style(&self) -> SetCursorStyle {
373        self.cursor_style
374    }
375
376    /// Sets the cursor visibility
377    pub fn set_cursor_visible(&mut self, visible: bool) {
378        self.cursor_visible = visible;
379    }
380
381    /// Sets the cursor position, clamping to window bounds
382    pub fn set_cursor(&mut self, pos: Vec2) {
383        let size = self.size();
384        self.cursor.x = pos.x.min(size.x.saturating_sub(1));
385        self.cursor.y = pos.y.min(size.y.saturating_sub(1));
386    }
387
388    /// Sets the cursor style
389    pub fn set_cursor_style(&mut self, style: SetCursorStyle) {
390        self.cursor_style = style;
391    }
392
393    /// Move the cursor by a given distance
394    pub fn move_cursor(&mut self, x: i16, y: i16) {
395        let size = self.size();
396        self.cursor.x = self
397            .cursor
398            .x
399            .saturating_add_signed(x)
400            .min(size.x.saturating_sub(1));
401        self.cursor.y = self
402            .cursor
403            .y
404            .saturating_add_signed(y)
405            .min(size.y.saturating_sub(1));
406    }
407
408    pub fn mouse_pos(&self) -> Vec2 {
409        self.mouse_pos
410    }
411
412    /// Pushes an event into the state
413    /// Could be usefull with a custom event loop
414    /// or for keyboard control from elsewhere
415    pub fn insert_event(&mut self, event: Event) {
416        match event {
417            Event::Resize(width, height) => {
418                if self.inline.is_none() {
419                    self.buffers = [
420                        Buffer::new_filled((width, height), ' '),
421                        Buffer::new_filled((width, height), ' '),
422                    ];
423                    self.just_resized = true;
424                }
425            }
426            Event::Mouse(MouseEvent { column, row, .. }) => self.mouse_pos = vec2(column, row),
427            _ => {}
428        }
429
430        self.events.push(event);
431    }
432
433    /// Clears events, usefull for handling issues with
434    /// custom event insertions or handlers
435    pub fn clear_events(&mut self) {
436        self.events.clear();
437    }
438
439    /// Returns the current event for the frame, as a reference.
440    pub fn events(&self) -> &Vec<Event> {
441        &self.events
442    }
443
444    /// Returns true if the mouse cursor is hovering the given rect.
445    pub fn hover<V: Into<Vec2>>(&self, loc: V, size: V) -> io::Result<bool> {
446        let loc = loc.into();
447        let size = size.into();
448        let pos: Vec2 = self.mouse_pos();
449        Ok(pos.x <= loc.x + size.x && pos.x >= loc.x && pos.y <= loc.y + size.y && pos.y >= loc.y)
450    }
451
452    pub fn io(&mut self) -> &mut Stdout {
453        &mut self.io
454    }
455}
456
457/// A macro that allows you to quickly check an event based off of a pattern
458/// Takes in the window, a pattern for the if let statement, and finally a closure.
459/// This closure could be anything that returns a bool.
460///
461/// Underneath, the event! macro runs an if let on your pattern checking for any of the
462/// Events to be true from your given closure.
463/**
464Example
465```rust, no_run
466# use ascii_forge::prelude::*;
467# fn main() -> std::io::Result<()> {
468# let mut window = Window::init()?;
469event!(window, Event::Key(e) => e.code == KeyCode::Char('q'));
470# Ok(())
471# }
472```
473*/
474#[macro_export]
475macro_rules! event {
476    ($window:expr, $event_type:pat => $($closure:tt)*) => {
477        $window.events().iter().any(|e| {
478            if let $event_type = e {
479                $($closure)*
480            } else {
481                false
482            }
483        })
484    };
485}
486
487/// Enables a panic hook to help you terminal still look pretty.
488pub fn handle_panics() {
489    let original_hook = take_hook();
490    set_hook(Box::new(move |e| {
491        Window::new(io::stdout())
492            .expect("Window should have created for panic")
493            .restore()
494            .expect("Window should have exited for panic");
495        original_hook(e);
496    }))
497}
498
499impl Drop for Window {
500    fn drop(&mut self) {
501        self.restore().expect("Restoration should have succeded");
502    }
503}