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            Hide,
126            DisableLineWrap,
127        )?;
128        Window::new(stdout)
129    }
130
131    /// Enables the kitty keyboard protocol
132    pub fn keyboard(&mut self) -> io::Result<()> {
133        if let Ok(t) = terminal::supports_keyboard_enhancement() {
134            if !t {
135                return Err(io::Error::new(
136                    io::ErrorKind::Unsupported,
137                    "Terminal doesn't support the kitty keyboard protocol",
138                ));
139            }
140            if let Some(inline) = &mut self.inline {
141                inline.kitty = true;
142            } else {
143                execute!(
144                    self.io(),
145                    PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::all())
146                )?;
147            }
148            Ok(())
149        } else {
150            Err(io::Error::new(
151                io::ErrorKind::Unsupported,
152                "Terminal doesn't support the kitty keyboard protocol",
153            ))
154        }
155    }
156
157    /// Returns the active Buffer, as a reference.
158    pub fn buffer(&self) -> &Buffer {
159        &self.buffers[self.active_buffer]
160    }
161
162    /// Returns the active Buffer, as a mutable reference.
163    pub fn buffer_mut(&mut self) -> &mut Buffer {
164        &mut self.buffers[self.active_buffer]
165    }
166
167    /// Swaps the buffers, clearing the old buffer. Used automatically by the window's update method.
168    pub fn swap_buffers(&mut self) {
169        self.active_buffer = 1 - self.active_buffer;
170        self.buffers[self.active_buffer].fill(' ');
171    }
172
173    /// Returns the current known size of the buffer's window.
174    pub fn size(&self) -> Vec2 {
175        self.buffer().size()
176    }
177
178    /// Restores the window to it's previous state from before the window's init method.
179    /// If the window is inline, restore the inline render
180    pub fn restore(&mut self) -> io::Result<()> {
181        if terminal::supports_keyboard_enhancement().is_ok() {
182            queue!(self.io, PopKeyboardEnhancementFlags)?;
183        }
184        if let Some(inline) = &self.inline {
185            execute!(
186                self.io,
187                DisableMouseCapture,
188                DisableFocusChange,
189                PopKeyboardEnhancementFlags,
190                Show,
191            )?;
192            if terminal::size()?.1 != inline.start + 1 {
193                print!(
194                    "{}",
195                    "\n".repeat(self.buffers[self.active_buffer].size().y as usize)
196                );
197            }
198            disable_raw_mode()?;
199            Ok(())
200        } else {
201            execute!(
202                self.io,
203                PopKeyboardEnhancementFlags,
204                LeaveAlternateScreen,
205                DisableMouseCapture,
206                DisableFocusChange,
207                Show,
208                EnableLineWrap,
209            )?;
210            disable_raw_mode()
211        }
212    }
213
214    /// 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.
215    pub fn render(&mut self) -> io::Result<()> {
216        if self.inline.is_some() {
217            if !self.inline.as_ref().expect("Inline should be some").active {
218                // Make room for the inline render
219                print!("{}", "\n".repeat(self.buffer().size().y as usize));
220
221                enable_raw_mode()?;
222
223                execute!(
224                    self.io,
225                    EnableMouseCapture,
226                    EnableFocusChange,
227                    DisableLineWrap,
228                    Hide,
229                )?;
230                if self.inline.as_ref().expect("Inline should be some").kitty {
231                    execute!(
232                        self.io,
233                        PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::all())
234                    )?;
235                }
236                let inline = self.inline.as_mut().expect("Inline should be some");
237                inline.active = true;
238                inline.start = cursor::position()?.1;
239            }
240
241            for (loc, cell) in
242                self.buffers[1 - self.active_buffer].diff(&self.buffers[self.active_buffer])
243            {
244                queue!(
245                    self.io,
246                    cursor::MoveTo(
247                        loc.x,
248                        self.inline.as_ref().expect("Inline should be some").start
249                            - self.buffers[self.active_buffer].size().y
250                            + loc.y
251                    ),
252                    Print(cell),
253                )?;
254            }
255
256            queue!(
257                self.io,
258                cursor::MoveTo(
259                    0,
260                    self.inline.as_ref().expect("Inline should be some").start
261                        - self.buffers[self.active_buffer].size().y
262                )
263            )?;
264        } else {
265            if self.just_resized {
266                self.just_resized = false;
267                let cell = self.buffers[self.active_buffer].size();
268                for x in 0..cell.x {
269                    for y in 0..cell.y {
270                        let cell = self.buffers[self.active_buffer]
271                            .get((x, y))
272                            .expect("Cell should be in bounds");
273                        queue!(self.io, cursor::MoveTo(x, y), Print(cell))?;
274                    }
275                }
276            }
277
278            for (loc, cell) in
279                self.buffers[1 - self.active_buffer].diff(&self.buffers[self.active_buffer])
280            {
281                queue!(self.io, cursor::MoveTo(loc.x, loc.y), Print(cell))?;
282            }
283        }
284        Ok(())
285    }
286
287    /// Handles events, and renders the screen.
288    pub fn update(&mut self, poll: Duration) -> io::Result<()> {
289        // Render Window
290        self.render()?;
291        self.swap_buffers();
292        self.render_cursor()?;
293        // Flush Render To Stdout
294        self.io.flush()?;
295        // Poll For Events
296        self.handle_event(poll)?;
297        Ok(())
298    }
299
300    pub fn render_cursor(&mut self) -> io::Result<()> {
301        // Get the current cursor position
302        let cursor_pos = cursor::position()?;
303        if self.cursor_style != self.last_cursor.2
304            || self.cursor != cursor_pos.into()
305            || self.cursor != self.last_cursor.1
306            || self.cursor_visible != self.last_cursor.0
307        {
308            if self.cursor_visible {
309                let cursor = self.cursor;
310                let style = self.cursor_style;
311
312                // Calculate the actual position based on inline rendering
313                let actual_pos = if let Some(inline) = &self.inline {
314                    vec2(
315                        cursor.x,
316                        inline.start - self.buffers[self.active_buffer].size().y + cursor.y,
317                    )
318                } else {
319                    cursor
320                };
321
322                queue!(self.io(), MoveTo(actual_pos.x, actual_pos.y), style, Show)?;
323            } else {
324                queue!(self.io(), Hide)?;
325            }
326        }
327        self.last_cursor = (self.cursor_visible, self.cursor, self.cursor_style);
328        Ok(())
329    }
330
331    /// Handles events. Used automatically by the update method, so no need to use it unless update is being used.
332    pub fn handle_event(&mut self, poll: Duration) -> io::Result<()> {
333        self.events = vec![];
334        if event::poll(poll)? {
335            // Get all queued events
336            while event::poll(Duration::ZERO)? {
337                let event = event::read()?;
338                match event {
339                    Event::Resize(width, height) => {
340                        if self.inline.is_none() {
341                            self.buffers = [
342                                Buffer::new_filled((width, height), ' '),
343                                Buffer::new_filled((width, height), ' '),
344                            ];
345                            self.just_resized = true;
346                        }
347                    }
348                    Event::Mouse(MouseEvent { column, row, .. }) => {
349                        self.mouse_pos = vec2(column, row)
350                    }
351                    _ => {}
352                }
353                self.events.push(event);
354            }
355        }
356        Ok(())
357    }
358
359    /// Returns whether the cursor is visible
360    pub fn cursor_visible(&self) -> bool {
361        self.cursor_visible
362    }
363
364    /// Returns the current cursor position
365    pub fn cursor(&self) -> Vec2 {
366        self.cursor
367    }
368
369    /// Returns the current cursor style
370    pub fn cursor_style(&self) -> SetCursorStyle {
371        self.cursor_style
372    }
373
374    /// Sets the cursor visibility
375    pub fn set_cursor_visible(&mut self, visible: bool) {
376        self.cursor_visible = visible;
377    }
378
379    /// Sets the cursor position, clamping to window bounds
380    pub fn set_cursor(&mut self, pos: Vec2) {
381        let size = self.size();
382        self.cursor.x = pos.x.min(size.x.saturating_sub(1));
383        self.cursor.y = pos.y.min(size.y.saturating_sub(1));
384    }
385
386    /// Sets the cursor style
387    pub fn set_cursor_style(&mut self, style: SetCursorStyle) {
388        self.cursor_style = style;
389    }
390
391    /// Move the cursor by a given distance
392    pub fn move_cursor(&mut self, x: i16, y: i16) {
393        let size = self.size();
394        self.cursor.x = self
395            .cursor
396            .x
397            .saturating_add_signed(x)
398            .min(size.x.saturating_sub(1));
399        self.cursor.y = self
400            .cursor
401            .y
402            .saturating_add_signed(y)
403            .min(size.y.saturating_sub(1));
404    }
405
406    pub fn mouse_pos(&self) -> Vec2 {
407        self.mouse_pos
408    }
409
410    /// Pushes an event into the state
411    /// Could be usefull with a custom event loop
412    /// or for keyboard control from elsewhere
413    pub fn insert_event(&mut self, event: Event) {
414        self.events.push(event);
415    }
416
417    /// Returns the current event for the frame, as a reference.
418    pub fn events(&self) -> &Vec<Event> {
419        &self.events
420    }
421
422    /// Returns true if the mouse cursor is hovering the given rect.
423    pub fn hover<V: Into<Vec2>>(&self, loc: V, size: V) -> io::Result<bool> {
424        let loc = loc.into();
425        let size = size.into();
426        let pos: Vec2 = self.mouse_pos();
427        Ok(pos.x <= loc.x + size.x && pos.x >= loc.x && pos.y <= loc.y + size.y && pos.y >= loc.y)
428    }
429
430    pub fn io(&mut self) -> &mut Stdout {
431        &mut self.io
432    }
433}
434
435/// A macro that allows you to quickly check an event based off of a pattern
436/// Takes in the window, a pattern for the if let statement, and finally a closure.
437/// This closure could be anything that returns a bool.
438///
439/// Underneath, the event! macro runs an if let on your pattern checking for any of the
440/// Events to be true from your given closure.
441/**
442Example
443```rust, no_run
444# use ascii_forge::prelude::*;
445# fn main() -> std::io::Result<()> {
446# let mut window = Window::init()?;
447event!(window, Event::Key(e) => e.code == KeyCode::Char('q'));
448# Ok(())
449# }
450```
451*/
452#[macro_export]
453macro_rules! event {
454    ($window:expr, $event_type:pat => $($closure:tt)*) => {
455        $window.events().iter().any(|e| {
456            if let $event_type = e {
457                $($closure)*
458            } else {
459                false
460            }
461        })
462    };
463}
464
465/// Enables a panic hook to help you terminal still look pretty.
466pub fn handle_panics() {
467    let original_hook = take_hook();
468    set_hook(Box::new(move |e| {
469        Window::new(io::stdout())
470            .expect("Window should have created for panic")
471            .restore()
472            .expect("Window should have exited for panic");
473        original_hook(e);
474    }))
475}
476
477impl Drop for Window {
478    fn drop(&mut self) {
479        self.restore().expect("Restoration should have succeded");
480    }
481}