simple_term_renderer/
rds.rs

1/*
2
3    MIT License
4
5    Copyright (c) 2022 Siandfrance
6
7    Permission is hereby granted, free of charge, to any person obtaining a copy
8    of this software and associated documentation files (the "Software"), to deal
9    in the Software without restriction, including without limitation the rights
10    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11    copies of the Software, and to permit persons to whom the Software is
12    furnished to do so, subject to the following conditions:
13
14    The above copyright notice and this permission notice shall be included in all
15    copies or substantial portions of the Software.
16
17    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23    SOFTWARE.
24
25*/
26
27
28extern crate libc;
29
30use crate::math::Vec2;
31use crate::img::{BlendMode, Color, Image};
32use crate::input::Input;
33use crate::screen_buffer::*;
34
35use termios::*;
36
37use std::mem;
38
39use std::io::{stdout, Write};
40
41use std::panic;
42use std::backtrace::Backtrace;
43
44use std::thread;
45use std::sync::{mpsc, Barrier, Arc, Mutex};
46
47use std::io::stdin;
48use std::os::unix::io::AsRawFd;
49
50const NCCS: usize = 32;
51
52
53/// csi macro rule
54macro_rules! csi {
55    ($( $l:expr ),*) => { concat!("\x1b[", $( $l ),*) };
56}
57
58
59/// Commands that are sent to the rendering server by the Renderer singleton.
60enum RenderingDirective {
61    DrawLine(Vec2, Vec2, Color),
62    DrawRect(Vec2, Vec2, Color),
63    DrawRectBoudary(Vec2, Vec2, Color),
64    DrawEllipseBoudary(Vec2, Vec2, Color),
65    DrawPoint(Vec2, Color),
66
67    DrawImage(Arc<Mutex<Image>>, Vec2, Vec2, Vec2, Option<Color>),
68    DrawWholeImageAlpha(Arc<Mutex<Image>>, Vec2, Color),
69    DrawWholeImage(Arc<Mutex<Image>>, Vec2),
70
71    PrintTextRaw(String, Vec2, CharBackgroundMode, CharForegroundMode),
72
73    ClearColor(Color),
74    ClearText,
75
76    UpdateScreenSize(Vec2),
77    BeginFrame,
78    PushFrame
79}
80
81
82/// This is the core of the library. It will send commands to the rendering server to print on screen.
83/// 
84/// # Usage
85/// 
86/// ```
87/// // get the renderer
88/// let rdr = Renderer::get();
89/// 
90/// ...
91/// 
92/// // start drawing on a frame
93/// rdr.begin_draw();
94/// 
95/// ... // use drawing functions (eg. draw_rect, draw_point...)
96/// 
97/// rdr.end_draw(); // this pushes the frame to the screen
98/// 
99/// ...
100/// 
101/// Renderer::exit(); // to quit the program and reset terminal settings
102/// ```
103/// 
104/// Screen coordinates start in the top left at (0, 0)
105pub struct Renderer {
106    termios: Termios,
107    default_c_lflags: u32,
108    default_c_cc: [u8; NCCS],
109
110    building_frame: bool,
111    prev_screen_size: Vec2,
112
113    _server_handle: Option<thread::JoinHandle<()>>,
114    sender: mpsc::Sender<RenderingDirective>,
115
116    frame_barrier: Arc<Barrier>
117}
118
119
120/// Renderer singleton
121static mut RENDERER: Option<Renderer> = None;
122
123
124impl Renderer {
125
126    /// Creates the Input singleton, will only be called once
127    fn init() -> Renderer {
128        let stdinfd = stdin().as_raw_fd();
129
130        let mut termios = match Termios::from_fd(stdinfd) {
131            Ok(t)  => t,
132            Err(_) => panic!("Could not read stdin fd")
133        };
134
135        // save and update settings
136        let default_c_lflags = termios.c_lflag;
137        let default_c_cc = termios.c_cc;
138
139        termios.c_lflag &= !(ECHO | ICANON | ISIG);
140        termios.c_cc[VMIN] = 1;
141        termios.c_cc[VTIME] = 0;
142
143        tcsetattr(stdinfd, TCSANOW, &mut termios).expect("could not set stdin attributes");
144        
145        print!("{}{}", 
146            csi!("?25l"),                                   // hide cursor
147            csi!("?1049h")                                 // use alternate screen buffer
148        );
149        stdout().flush().expect("Could not write to stdout"); 
150
151        // Exit when panicking + print backtrace
152        panic::set_hook(Box::new(|panic_info| {
153            let backtrace = Backtrace::capture();
154            eprintln!("{}", backtrace);
155            eprintln!("{}", panic_info);
156            Renderer::exit();
157        }));
158
159        // setup and start server
160        let (rx, tx) = mpsc::channel();
161        let barrier = Arc::new(Barrier::new(2));
162        let frame_barrier = Arc::clone(&barrier);
163
164        let handle = thread::spawn(move || {
165            let mut screen_size = Renderer::get_size();
166            let mut screen     : ScreeBuffer = ScreeBuffer::new((0, 0));
167            let mut prev_screen: ScreeBuffer = ScreeBuffer::new((0, 0));
168
169            let mut back: Color = Color::BLACK;
170            let mut fore: Color = Color::BLACK;
171            print!("{:-}{:+}", back, fore);
172
173
174            loop {
175                match tx.recv().expect("RenderingServer channel was destroyed") {
176                    RenderingDirective::DrawLine(p1, p2, c) => screen.line(p1, p2, c),
177                    RenderingDirective::DrawRect(p, s, c) => screen.rect(p, s, c),
178                    RenderingDirective::DrawRectBoudary(p, s, c) => screen.rect_boudary(p, s, c),
179                    RenderingDirective::DrawEllipseBoudary(center, s, c) => screen.ellipse_boundary(center, s, c),
180                    RenderingDirective::DrawPoint(p, c) => screen.point(p, c),
181
182                    RenderingDirective::DrawImage(img, pos, size, off, alpha) => screen.image(&(*img.lock().unwrap()), pos, size, off, alpha),
183                    RenderingDirective::DrawWholeImageAlpha(img, pos, alpha) => screen.whole_image_alpha(&(*img.lock().unwrap()), pos, alpha),
184                    RenderingDirective::DrawWholeImage(img, pos) => screen.whole_image(&(*img.lock().unwrap()), pos),
185
186                    RenderingDirective::PrintTextRaw(text, pos, back_mode, fore_mode) => screen.print_text_raw(text, pos, back_mode, fore_mode),
187
188                    RenderingDirective::ClearColor(c) => screen.clear_color(c),
189                    RenderingDirective::ClearText => screen.clear_text(),
190
191                    RenderingDirective::UpdateScreenSize(size) => {
192                        screen_size = size;
193                        screen.raw_resize(size); // TODO: raw_resize
194                    }
195
196                    RenderingDirective::BeginFrame => {frame_barrier.wait(); ()},
197                    RenderingDirective::PushFrame => {
198                        // position cursor
199                        print!("\x1b[H");
200
201                        let mut skiped = false;
202
203                        for j in (0..screen_size.y).step_by(2) {
204                            for i in 0..screen_size.x {
205                                let pos1 = vec2!(i, j);
206                                let pos2 = vec2!(i, j + 1);
207
208                                let color1 = screen.get_color(pos1);
209                                let color2 = screen.get_color(pos2);
210                                let char_data = screen.get_char_data(pos1);
211
212                                let prev_color1 = prev_screen.get_color(pos1);
213                                let prev_color2 = prev_screen.get_color(pos2);
214                                let prev_char_data = prev_screen.get_char_data(pos1);
215                                
216                                // Can this pixel be skipped ?
217                                if screen.size() == prev_screen.size()
218                                        && char_data == prev_char_data
219                                        && color1 == prev_color1
220                                        && color2 == prev_color2 {
221                                    skiped = true;
222                                    continue;
223                                }
224
225                                // If the previous pixel was a skip, go to the current pixel
226                                if skiped {
227                                    print!("\x1b[{};{}H", j/2 + 1, i + 1);
228                                    skiped = false;
229                                }
230
231
232                                match char_data.c {
233                                    Some(c) => {
234                                        // Get the color that needs to be displayed
235                                        let background_color = match char_data.bg_mode {
236                                            CharBackgroundMode::Blend => Color::blend(color1, color2, BlendMode::Add),
237                                            CharBackgroundMode::Colored(c) => c
238                                        };
239                                        let foreground_color = match char_data.fg_mode {
240                                            CharForegroundMode::Opposite => {
241                                                // let (h, s, l) = background_color.get_okhsl();
242                                                // Color::okhsl(1.0 - h, s, 1.0 - l)
243                                                let (_, _, l) = background_color.get_okhsl();
244                                                if l > 0.5 {
245                                                    Color::BLACK
246                                                } else {
247                                                    Color::WHITE
248                                                }
249                                            },
250                                            CharForegroundMode::Colored(c) => c
251                                        };
252
253                                        // Update foreground and background color
254                                        if back != background_color {
255                                            back = background_color;
256                                            print!("{:-}", back);
257                                        }
258                                        if fore != foreground_color {
259                                            fore = foreground_color;
260                                            print!("{:+}", fore);
261                                        }
262                                        
263                                        // Print the character
264                                        print!("{}", c);
265                                    }
266                                    None => {
267                                        
268                                
269                                        // update color
270                                        if color1 != back && color1 != fore && color2 == back {
271                                            fore = color1;
272                                            print!("{:+}", fore);
273                                        } else if color1 != back && color1 != fore && color2 == fore {
274                                            back = color1;
275                                            print!("{:-}", back);
276                                        } else if color2 != back && color2 != fore && color1 == back {
277                                            fore = color2;
278                                            print!("{:+}", fore);
279                                        } else if color2 != back && color2 != fore && color1 == fore {
280                                            back = color2;
281                                            print!("{:-}", back);
282                                        } else if color1 != back && color1 != fore && color2 != back && color2 != fore {
283                                            fore = color1;
284                                            back = color2;
285                                            print!("{:+}", fore);
286                                            print!("{:-}", back);
287                                        }
288
289                                        // print pixel
290                                        if color1 == back && color2 == back {
291                                            print!(" ");
292                                        } else if color1 == back && color2 == fore {
293                                            print!("▄");
294                                        } else if color1 == fore && color2 == back {
295                                            print!("▀");
296                                        } else if color1 == fore && color2 == fore {
297                                            print!("█");
298                                        }
299                                    }
300                                }
301                            }
302                        }
303                        stdout().flush().expect("Could not write to stdout");
304                        prev_screen = screen.clone();
305                    }
306                }
307            }
308        });
309
310        Renderer {
311            termios: termios,
312            default_c_lflags: default_c_lflags,
313            default_c_cc: default_c_cc,
314
315            building_frame: false,
316            prev_screen_size: Vec2::ZERO,
317
318            _server_handle: Some(handle),
319            sender: rx,
320
321            frame_barrier: barrier
322        }
323    }
324
325
326    /// Exits the program and reset terminal setttings (should be called before the program ends).
327    pub fn exit() {
328        unsafe {
329            RENDERER = None;
330        }
331    }
332
333
334    /// Returns the Renderer instance.
335    pub fn get() -> &'static mut Renderer {
336        unsafe {
337            match &mut RENDERER {
338                None => { // construct the renderer, and initialize
339                    RENDERER = Some(Renderer::init());
340                    Renderer::get()
341                }
342                Some(r) => r
343            }
344        }
345    }
346
347
348    /// Returns the screen dimension.
349    /// ```
350    /// let size = Renderer::get_size();
351    /// 
352    /// size.x // width of the screen
353    /// size.y // height of the screen
354    /// ```
355    pub fn get_size() -> Vec2 {
356        unsafe {
357            let mut size: TermSize = mem::zeroed();
358            libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut size as *mut _);
359            vec2!(size.col as i32, 2 * size.row as i32)
360        }
361    }
362
363    
364    /// panics if we are not in a draw loop
365    fn can_draw(&self) {
366        if !self.building_frame { panic!("drawing outside of a frame build (call begin_draw)"); }
367    }
368
369
370    /// Starts drawing a frame.
371    /// 
372    /// Will panic if called twice before an end_draw
373    pub fn begin_draw(&mut self) {
374        if self.building_frame {
375            panic!("begin_draw called when already building a frame");
376        }
377        self.building_frame = true;
378        let new_size = Renderer::get_size();
379        if self.prev_screen_size != new_size {
380            self.sender.send(RenderingDirective::UpdateScreenSize(new_size)).expect("Rendering thread stopped");
381            self.prev_screen_size = new_size;
382        }
383
384        self.sender.send(RenderingDirective::BeginFrame).expect("Rendering thread stopped");
385        self.frame_barrier.wait();
386    }
387
388
389    /// Ends drawing a frame and pushes it to the screen.
390    pub fn end_draw(&mut self) {
391        if !self.building_frame {
392            panic!("end_draw called when already building a frame");
393        }
394        self.building_frame = false;
395        self.sender.send(RenderingDirective::PushFrame).expect("Rendering thread stopped");
396    }
397
398
399    /// Clears the screen (color and text).
400    pub fn clear(&mut self, c: Color) {
401        self.can_draw();
402        self.sender.send(RenderingDirective::ClearColor(c)).expect("Rendering thread stopped");
403        self.sender.send(RenderingDirective::ClearText).expect("Rendering thread stopped");
404    }
405
406
407    /// Sets all the pixels' color in the screen to `c` (does not clear text).
408    pub fn clear_color(&mut self, c: Color) {
409        self.can_draw();
410        self.sender.send(RenderingDirective::ClearColor(c)).expect("Rendering thread stopped");
411    }
412
413
414    /// Removes all text from the screen.
415    pub fn clear_text(&mut self) {
416        self.can_draw();
417        self.sender.send(RenderingDirective::ClearText).expect("Rendering thread stopped");
418    }
419
420
421    /// Draws a line of color `c` between `p1` and `p2`.
422    pub fn draw_line<A, B>(&mut self, p1: A, p2: B, c: Color) 
423        where A: AsRef<Vec2>, B: AsRef<Vec2>
424    {
425        self.can_draw();
426        self.sender.send(RenderingDirective::DrawLine(*p1.as_ref(), *p2.as_ref(), c))
427            .expect("Rendering thread stopped");
428    }
429
430
431    /// Draws a rectangle of color `c` and of size `s`. 
432    /// `p` is the coordinate of the top left corner of the rectangle.
433    pub fn draw_rect<A, B>(&mut self, p: A, s: B, c: Color) 
434        where A: AsRef<Vec2>, B: AsRef<Vec2>
435    {
436        self.can_draw();
437        self.sender.send(RenderingDirective::DrawRect(*p.as_ref(), *s.as_ref(), c))
438            .expect("Rendering thread stopped");
439    }
440
441
442    /// Same as `draw_rect` but draws only the four sides of the rectangle.
443    pub fn draw_rect_boundary<A, B>(&mut self, p: A, s: B, c: Color) 
444        where A: AsRef<Vec2>, B: AsRef<Vec2>
445    {
446        self.can_draw();
447        self.sender.send(RenderingDirective::DrawRectBoudary(*p.as_ref(), *s.as_ref(), c))
448            .expect("Rendering thread stopped");
449    }
450
451
452    /// Draws an ellipse of color `col`. `c` is the center of the ellipse and `s` is the size of the rectangle
453    /// in which the ellipse is inscribed.
454    pub fn draw_ellipse_boundary<A, B>(&mut self, c: A, s: B, col: Color) 
455        where A: AsRef<Vec2>, B: AsRef<Vec2>
456    {
457        self.can_draw();
458        self.sender.send(RenderingDirective::DrawEllipseBoudary(*c.as_ref(), *s.as_ref(), col))
459            .expect("Rendering thread stopped");
460    }
461
462
463    /// Sets the color of the pixel at `p` to `c`.
464    pub fn draw_point<A>(&mut self, p: A, c: Color) 
465        where A: AsRef<Vec2>
466    {
467        self.can_draw();
468        self.sender.send(RenderingDirective::DrawPoint(*p.as_ref(), c)).expect("Rendering thread stopped");
469    }
470
471
472    /// Draws an image at position `pos`. 
473    /// 
474    /// Negative size results in flipped image. Alpha is used to ignore a given color while drawing.
475    pub fn draw_image<A, B, C>(&mut self, 
476        img: Arc<Mutex<Image>>, pos: A, size: B, offset: C, alpha: Option<Color>) 
477        where A: AsRef<Vec2>, B: AsRef<Vec2>, C: AsRef<Vec2>
478    {
479        self.can_draw();
480        self.sender.send(RenderingDirective::DrawImage(img, *pos.as_ref(), *size.as_ref(), *offset.as_ref(), alpha))
481            .expect("Rendering thread stopped");
482    }
483
484
485    /// Draws the whole image at `pos`, ignoring the color `alpha`.
486    /// 
487    /// Equivalent to:
488    /// ```
489    /// rdr.image(img, pos, img.size(), Vec2::ZERO, Some(alpha));
490    /// ```
491    pub fn draw_whole_image_alpha<A>(&mut self, img: Arc<Mutex<Image>>, pos: A, alpha: Color) 
492        where A: AsRef<Vec2>
493    {
494        self.can_draw();
495        self.sender.send(RenderingDirective::DrawWholeImageAlpha(img, *pos.as_ref(), alpha))
496            .expect("Rendering thread stopped");
497    }
498
499
500    /// Draws the whole image at `pos`.
501    /// 
502    /// Equivalent to:
503    /// ```
504    /// rdr.image(img, pos, img.size(), Vec2::ZERO, None);
505    /// ```
506    pub fn draw_whole_image<A>(&mut self, img: Arc<Mutex<Image>>, pos: A) 
507        where A: AsRef<Vec2>
508    {
509        self.can_draw();
510        self.sender.send(RenderingDirective::DrawWholeImage(img, *pos.as_ref())).expect("Rendering thread stopped");
511    }
512
513
514    pub fn print_text_raw<A>(&mut self, text: &String, pos: A, background_mode: CharBackgroundMode, foreground_mode: CharForegroundMode)
515        where A: AsRef<Vec2>
516    {
517        self.can_draw();
518        self.sender.send(RenderingDirective::PrintTextRaw(text.clone(), *pos.as_ref(), background_mode, foreground_mode))
519            .expect("Rendering thread stopped");
520    }
521
522
523    pub fn print_blended_text_raw<A>(&mut self, text: &String, pos: A)
524        where A: AsRef<Vec2>
525    {
526        self.print_text_raw(
527            text,
528            pos,
529            CharBackgroundMode::Blend,
530            CharForegroundMode::Opposite
531        );
532    }
533
534
535    pub fn print_colored_text_raw<A>(&mut self, text: &String, pos: A, background_color: Color, foreground_color: Color)
536        where A: AsRef<Vec2>
537    {
538        self.print_text_raw(
539            text,
540            pos,
541            CharBackgroundMode::Colored(background_color),
542            CharForegroundMode::Colored(foreground_color)
543        )
544    }
545
546
547
548    /// Rings the terminal bell. Can only be called during the creation of a frame
549    /// 
550    /// Technical note: the bell will ring when calling `end_draw`
551    pub fn ring_bell(&self) {
552        self.can_draw();
553        print!("\x07");
554    }
555}
556
557
558impl Drop for Renderer {
559
560    /// When the renderer singleton is droped, reset terminal settings and exit.
561    fn drop(&mut self) {
562        // return settings to default
563        self.termios.c_cc = self.default_c_cc;
564        self.termios.c_lflag = self.default_c_lflags;
565
566        let stdinfd = stdin().as_raw_fd();
567        tcsetattr(stdinfd, TCSANOW, &mut self.termios).expect("could not set stdin attributes");
568
569        print!("{}{}",
570            csi!("?25h"),                                   // show cursor
571            csi!("?1049l")                                  // use main screen buffer
572        );
573        stdout().flush().expect("Could not write to stdout");
574        Input::disable_mouse();
575
576        std::process::exit(0);
577    }
578}
579
580
581struct TermSize {
582    row: libc::c_ushort,
583    col: libc::c_ushort,
584    _x : libc::c_ushort,
585    _y : libc::c_ushort
586}