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    cursor_dirty: bool,
53
54    // Input Helpers,
55    mouse_pos: Vec2,
56    // Inlining
57    inline: Option<Inline>,
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: [
75                Buffer::new_filled(size()?, ' '),
76                Buffer::new_filled(size()?, ' '),
77            ],
78            active_buffer: 0,
79            events: vec![],
80            last_cursor: (false, vec2(0, 0), SetCursorStyle::SteadyBlock),
81            cursor_visible: false,
82            cursor_style: SetCursorStyle::SteadyBlock,
83            cursor: vec2(0, 0),
84            cursor_dirty: false,
85            mouse_pos: vec2(0, 0),
86            inline: None,
87            just_resized: false,
88        })
89    }
90
91    /// Creates a new window built for inline using the given Stdout and height.
92    pub fn new_inline(io: io::Stdout, height: u16) -> io::Result<Self> {
93        let size = vec2(size()?.0, height);
94        Ok(Self {
95            io,
96            buffers: [Buffer::new_filled(size, ' '), Buffer::new_filled(size, ' ')],
97            active_buffer: 0,
98            events: vec![],
99            last_cursor: (false, vec2(0, 0), SetCursorStyle::SteadyBlock),
100            cursor_visible: false,
101            cursor_style: SetCursorStyle::SteadyBlock,
102            cursor: vec2(0, 0),
103            cursor_dirty: false,
104            mouse_pos: vec2(0, 0),
105            inline: Some(Inline::default()),
106            just_resized: false,
107        })
108    }
109
110    /// Initializes a window that is prepared for inline rendering.
111    /// Height is the number of columns that your terminal will need.
112    pub fn init_inline(height: u16) -> io::Result<Self> {
113        let stdout = io::stdout();
114        assert!(stdout.is_tty());
115        Window::new_inline(stdout, height)
116    }
117
118    /// Initializes the window, and returns a new Window for your use.
119    pub fn init() -> io::Result<Self> {
120        enable_raw_mode()?;
121        let mut stdout = io::stdout();
122        assert!(stdout.is_tty());
123        execute!(
124            stdout,
125            EnterAlternateScreen,
126            EnableMouseCapture,
127            EnableFocusChange,
128            EnableBracketedPaste,
129            Hide,
130            DisableLineWrap,
131        )?;
132        Window::new(stdout)
133    }
134
135    /// Enables the kitty keyboard protocol
136    pub fn keyboard(&mut self) -> io::Result<()> {
137        if let Ok(t) = terminal::supports_keyboard_enhancement() {
138            if !t {
139                return Err(io::Error::new(
140                    io::ErrorKind::Unsupported,
141                    "Terminal doesn't support the kitty keyboard protocol",
142                ));
143            }
144            if let Some(inline) = &mut self.inline {
145                inline.kitty = true;
146            } else {
147                execute!(
148                    self.io(),
149                    PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::all())
150                )?;
151            }
152            Ok(())
153        } else {
154            Err(io::Error::new(
155                io::ErrorKind::Unsupported,
156                "Terminal doesn't support the kitty keyboard protocol",
157            ))
158        }
159    }
160
161    /// Returns the active Buffer, as a reference.
162    pub fn buffer(&self) -> &Buffer {
163        &self.buffers[self.active_buffer]
164    }
165
166    /// Returns the active Buffer, as a mutable reference.
167    pub fn buffer_mut(&mut self) -> &mut Buffer {
168        &mut self.buffers[self.active_buffer]
169    }
170
171    /// Swaps the buffers, clearing the old buffer. Used automatically by the window's update method.
172    pub fn swap_buffers(&mut self) {
173        self.active_buffer = 1 - self.active_buffer;
174        self.buffers[self.active_buffer].fill(' ');
175    }
176
177    /// Returns the current known size of the buffer's window.
178    pub fn size(&self) -> Vec2 {
179        self.buffer().size()
180    }
181
182    /// Restores the window to it's previous state from before the window's init method.
183    /// If the window is inline, restore the inline render
184    pub fn restore(&mut self) -> io::Result<()> {
185        if terminal::supports_keyboard_enhancement().is_ok() {
186            queue!(self.io, PopKeyboardEnhancementFlags)?;
187        }
188        if let Some(inline) = &self.inline {
189            execute!(
190                self.io,
191                DisableMouseCapture,
192                DisableFocusChange,
193                DisableBracketedPaste,
194                PopKeyboardEnhancementFlags,
195                Show,
196            )?;
197            if terminal::size()?.1 != inline.start + 1 {
198                print!(
199                    "{}",
200                    "\n".repeat(self.buffers[self.active_buffer].size().y as usize)
201                );
202            }
203            disable_raw_mode()?;
204            Ok(())
205        } else {
206            execute!(
207                self.io,
208                PopKeyboardEnhancementFlags,
209                LeaveAlternateScreen,
210                DisableMouseCapture,
211                DisableFocusChange,
212                DisableBracketedPaste,
213                Show,
214                EnableLineWrap,
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                    EnableBracketedPaste,
234                    DisableLineWrap,
235                    Hide,
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                let inline = self.inline.as_mut().expect("Inline should be some");
244                inline.active = true;
245                inline.start = cursor::position()?.1;
246            }
247
248            for (loc, cell) in
249                self.buffers[1 - self.active_buffer].diff(&self.buffers[self.active_buffer])
250            {
251                queue!(
252                    self.io,
253                    cursor::MoveTo(
254                        loc.x,
255                        self.inline.as_ref().expect("Inline should be some").start
256                            - self.buffers[self.active_buffer].size().y
257                            + loc.y
258                    ),
259                    Print(cell),
260                )?;
261
262                self.cursor_dirty = true;
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]
280                            .get((x, y))
281                            .expect("Cell should be in bounds");
282                        queue!(self.io, cursor::MoveTo(x, y), Print(cell))?;
283
284                        self.cursor_dirty = true;
285                    }
286                }
287            }
288
289            for (loc, cell) in
290                self.buffers[1 - self.active_buffer].diff(&self.buffers[self.active_buffer])
291            {
292                queue!(self.io, cursor::MoveTo(loc.x, loc.y), Print(cell))?;
293
294                self.cursor_dirty = true;
295            }
296        }
297        Ok(())
298    }
299
300    /// Handles events, and renders the screen.
301    pub fn update(&mut self, poll: Duration) -> io::Result<()> {
302        // Render Window
303        self.render()?;
304        self.swap_buffers();
305        self.render_cursor()?;
306        // Flush Render To Stdout
307        self.io.flush()?;
308        // Poll For Events
309        self.handle_event(poll)?;
310        Ok(())
311    }
312
313    pub fn render_cursor(&mut self) -> io::Result<()> {
314        // Get the current cursor position
315        if self.cursor_style != self.last_cursor.2
316            || self.cursor != self.last_cursor.1
317            || self.cursor_visible != self.last_cursor.0
318            || self.cursor_dirty
319        {
320            self.cursor_dirty = false;
321            if self.cursor_visible {
322                let cursor = self.cursor;
323                let style = self.cursor_style;
324
325                // Calculate the actual position based on inline rendering
326                let actual_pos = if let Some(inline) = &self.inline {
327                    vec2(
328                        cursor.x,
329                        inline.start - self.buffers[self.active_buffer].size().y + cursor.y,
330                    )
331                } else {
332                    cursor
333                };
334
335                queue!(self.io(), MoveTo(actual_pos.x, actual_pos.y), style, Show)?;
336            } else {
337                queue!(self.io(), Hide)?;
338            }
339        }
340        self.last_cursor = (self.cursor_visible, self.cursor, self.cursor_style);
341        Ok(())
342    }
343
344    /// Handles events. Used automatically by the update method, so no need to use it unless update is being used.
345    pub fn handle_event(&mut self, poll: Duration) -> io::Result<()> {
346        self.events = vec![];
347        if event::poll(poll)? {
348            // Get all queued events
349            while event::poll(Duration::ZERO)? {
350                let event = event::read()?;
351                match event {
352                    Event::Resize(width, height) => {
353                        if self.inline.is_none() {
354                            self.buffers = [
355                                Buffer::new_filled((width, height), ' '),
356                                Buffer::new_filled((width, height), ' '),
357                            ];
358                            self.just_resized = true;
359                        }
360                    }
361                    Event::Mouse(MouseEvent { column, row, .. }) => {
362                        self.mouse_pos = vec2(column, row)
363                    }
364                    _ => {}
365                }
366                self.events.push(event);
367            }
368        }
369        Ok(())
370    }
371
372    /// Returns whether the cursor is visible
373    pub fn cursor_visible(&self) -> bool {
374        self.cursor_visible
375    }
376
377    /// Returns the current cursor position
378    pub fn cursor(&self) -> Vec2 {
379        self.cursor
380    }
381
382    /// Returns the current cursor style
383    pub fn cursor_style(&self) -> SetCursorStyle {
384        self.cursor_style
385    }
386
387    /// Sets the cursor visibility
388    pub fn set_cursor_visible(&mut self, visible: bool) {
389        self.cursor_visible = visible;
390    }
391
392    /// Sets the cursor position, clamping to window bounds
393    pub fn set_cursor(&mut self, pos: Vec2) {
394        let size = self.size();
395        self.cursor.x = pos.x.min(size.x.saturating_sub(1));
396        self.cursor.y = pos.y.min(size.y.saturating_sub(1));
397    }
398
399    /// Sets the cursor style
400    pub fn set_cursor_style(&mut self, style: SetCursorStyle) {
401        self.cursor_style = style;
402    }
403
404    /// Move the cursor by a given distance
405    pub fn move_cursor(&mut self, x: i16, y: i16) {
406        let size = self.size();
407        self.cursor.x = self
408            .cursor
409            .x
410            .saturating_add_signed(x)
411            .min(size.x.saturating_sub(1));
412        self.cursor.y = self
413            .cursor
414            .y
415            .saturating_add_signed(y)
416            .min(size.y.saturating_sub(1));
417    }
418
419    pub fn mouse_pos(&self) -> Vec2 {
420        self.mouse_pos
421    }
422
423    /// Pushes an event into the state
424    /// Could be usefull with a custom event loop
425    /// or for keyboard control from elsewhere
426    pub fn insert_event(&mut self, event: Event) {
427        match event {
428            Event::Resize(width, height) => {
429                if self.inline.is_none() {
430                    self.buffers = [
431                        Buffer::new_filled((width, height), ' '),
432                        Buffer::new_filled((width, height), ' '),
433                    ];
434                    self.just_resized = true;
435                }
436            }
437            Event::Mouse(MouseEvent { column, row, .. }) => self.mouse_pos = vec2(column, row),
438            _ => {}
439        }
440
441        self.events.push(event);
442    }
443
444    /// Clears events, usefull for handling issues with
445    /// custom event insertions or handlers
446    pub fn clear_events(&mut self) {
447        self.events.clear();
448    }
449
450    /// Returns the current event for the frame, as a reference.
451    pub fn events(&self) -> &Vec<Event> {
452        &self.events
453    }
454
455    /// Returns true if the mouse cursor is hovering the given rect.
456    pub fn hover<V: Into<Vec2>>(&self, loc: V, size: V) -> io::Result<bool> {
457        let loc = loc.into();
458        let size = size.into();
459        let pos: Vec2 = self.mouse_pos();
460        Ok(pos.x <= loc.x + size.x && pos.x >= loc.x && pos.y <= loc.y + size.y && pos.y >= loc.y)
461    }
462
463    pub fn io(&mut self) -> &mut Stdout {
464        &mut self.io
465    }
466}
467
468/// A macro that allows you to quickly check an event based off of a pattern
469/// Takes in the window, a pattern for the if let statement, and finally a closure.
470/// This closure could be anything that returns a bool.
471///
472/// Underneath, the event! macro runs an if let on your pattern checking for any of the
473/// Events to be true from your given closure.
474/**
475Example
476```rust, no_run
477# use ascii_forge::prelude::*;
478# fn main() -> std::io::Result<()> {
479# let mut window = Window::init()?;
480event!(window, Event::Key(e) => e.code == KeyCode::Char('q'));
481# Ok(())
482# }
483```
484*/
485#[macro_export]
486macro_rules! event {
487    ($window:expr, $event_type:pat => $($closure:tt)*) => {
488        $window.events().iter().any(|e| {
489            if let $event_type = e {
490                $($closure)*
491            } else {
492                false
493            }
494        })
495    };
496}
497
498/// Enables a panic hook to help you terminal still look pretty.
499pub fn handle_panics() {
500    let original_hook = take_hook();
501    set_hook(Box::new(move |e| {
502        Window::new(io::stdout())
503            .expect("Window should have created for panic")
504            .restore()
505            .expect("Window should have exited for panic");
506        original_hook(e);
507    }))
508}
509
510impl Drop for Window {
511    fn drop(&mut self) {
512        self.restore().expect("Restoration should have succeded");
513    }
514}