Skip to main content

simpleterm/
terminal.rs

1use piston_window::{*, types::{Color, FontSize}};
2use std::{thread, time::{Duration, Instant}};
3
4use crate::{draw::*, text::*, TYPE_TIME};
5
6/// A terminal stores a PistonWindow, background and foreground colors,
7/// a font, fontsize, and glyph cache, and the current message and input strings.
8pub struct Terminal {
9    title: String,
10    active: bool,
11    /// The window that displays our terminal.
12    pub window: PistonWindow,
13    /// The background color of our terminal.
14    pub bg_color: Color,
15    /// The foreground color of our terminal.
16    pub fg_color: Color,
17    /// Whether or not to use scanlines
18    pub scanlines: bool,
19    glyphs: Glyphs,
20    font: String,
21    art_font: String,
22    /// The font size of normal text in our terminal.
23    pub font_size: FontSize,
24    /// The font size of art in our terminal.
25    pub art_font_size: FontSize,
26    art_mode: bool,
27    message: Vec<String>,
28    input: String,
29}
30
31impl Terminal {
32    /// Creates a new window with the given title, colors, and font info
33    /// 
34    /// ```no_run
35    /// # use simpleterm::text::*;
36    /// # use simpleterm::terminal::Terminal;
37    /// let mut term: Terminal = Terminal::new("simpleterm test", (800, 600), DARK_GREY, GOLD, "LeagueSpartan-Regular.ttf", 32);
38    /// ```
39    pub fn new(title: &str, size: (u32, u32), bg: Color, fg: Color, font: &str, font_size: u32) -> Terminal {
40        let mut new_window: PistonWindow = WindowSettings::new(title, size).exit_on_esc(true).build().unwrap();
41        let loaded_glyphs = load_font(&mut new_window, font);
42
43        Terminal {
44            title: String::from(title),
45            active: true,
46            window: new_window,
47            bg_color: bg,
48            fg_color: fg,
49            scanlines: true,
50            glyphs: loaded_glyphs,
51            font: String::from(font),
52            art_font: String::from("LeagueMono-Regular.ttf"),
53            font_size,
54            art_font_size: 10,
55            art_mode: false,
56            message: Vec::new(),
57            input: String::default(),
58        }
59    }
60
61    /// Types out the given message, then waits for the user to type something and returns Some(input string).
62    /// If the window is closed before input can be returned, returns None.
63    /// 
64    /// ```no_run
65    /// # use simpleterm::text::*;
66    /// # use simpleterm::terminal::Terminal;
67    /// # let mut term: Terminal = Terminal::new("simpleterm test", (800, 600), DARK_GREY, GOLD, "LeagueSpartan-Regular.ttf", 32);
68    /// let user_input: String = term.ask("This will wait for the user enter input!").unwrap();
69    /// ```
70    pub fn ask(&mut self, message: &str) -> Option<String> {
71        if self.active {
72            if self.art_mode {
73                self.glyphs = load_font(&mut self.window, &self.font);
74                self.art_mode = false;
75            }
76
77            self.new_message(message);
78            self.wait_for_input();
79            Some(self.input.clone())
80        } else {
81            None
82        }
83    }
84
85    /// Displays an ascii art string centered on the terminal. This uses 10pt font and a monospace font.
86    /// 
87    /// ```no_run
88    /// # use std::time::Duration;
89    /// # use simpleterm::{art::*, text::*};
90    /// # use simpleterm::terminal::Terminal;
91    /// # let mut term: Terminal = Terminal::new("simpleterm test", (800, 600), DARK_GREY, GOLD, "LeagueSpartan-Regular.ttf", 32);
92    /// term.display_art(GEO, Duration::from_secs(2));
93    /// ```
94    pub fn display_art(&mut self, art: &str, time: Duration) {
95        if self.active {
96            if !self.art_mode {
97                self.glyphs = load_font(&mut self.window, &self.art_font);
98                self.art_mode = true;
99            }
100
101            self.message = art.split('\n').map(String::from).collect();
102            self.input = String::default();
103            self.show_art(time);
104        }
105    }
106    
107    /// Types out the given message, then waits for the given amount of time to continue.
108    /// 
109    /// ```no_run
110    /// # use std::time::Duration;
111    /// # use simpleterm::text::*;
112    /// # use simpleterm::terminal::Terminal;
113    /// # let mut term: Terminal = Terminal::new("simpleterm test", (800, 600), DARK_GREY, GOLD, "LeagueSpartan-Regular.ttf", 32);
114    /// term.show("This will wait for 1 second!", Duration::from_secs(1));
115    /// ```
116    pub fn show(&mut self, message: &str, time: Duration) {
117        if self.active {
118            if self.art_mode {
119                self.glyphs = load_font(&mut self.window, &self.font);
120                self.art_mode = false;
121            }
122
123            self.new_message(message);
124            self.wait_for_timer(time);
125        }
126    }
127
128    /// Types out the given message, then waits for the user to press Enter to continue.
129    /// 
130    /// ```no_run
131    /// # use simpleterm::text::*;
132    /// # use simpleterm::terminal::Terminal;
133    /// # let mut term: Terminal = Terminal::new("simpleterm test", (800, 600), DARK_GREY, GOLD, "LeagueSpartan-Regular.ttf", 32);
134    /// term.tell("This will wait for the user to hit enter!");
135    /// ```
136    pub fn tell(&mut self, message: &str) {
137        if self.active {
138            if self.art_mode {
139                self.glyphs = load_font(&mut self.window, &self.font);
140                self.art_mode = false;
141            }
142
143            self.new_message(message);
144            self.input = String::from("Press Enter to Continue");
145            self.wait_for_continue();
146        }
147    }
148
149    /// Closes the current window and creates a new one with the given (x, y) Size.
150    /// 
151    /// ```no_run
152    /// # use simpleterm::text::*;
153    /// # use simpleterm::terminal::Terminal;
154    /// # let mut term: Terminal = Terminal::new("simpleterm test", (800, 600), DARK_GREY, GOLD, "LeagueSpartan-Regular.ttf", 32);
155    /// term.resize((800, 600).into());
156    /// ```
157    pub fn resize(&mut self, new_size: Size) {
158        if self.active {
159            let new_window: PistonWindow = WindowSettings::new(self.title.clone(), new_size).exit_on_esc(true).build().unwrap();
160            self.window = new_window;
161        }
162    }
163
164    /// Loads a new font from the given font filename and sets the given font size
165    /// 
166    /// ```no_run
167    /// # use simpleterm::text::*;
168    /// # use simpleterm::terminal::Terminal;
169    /// # let mut term: Terminal = Terminal::new("simpleterm test", (800, 600), DARK_GREY, GOLD, "LeagueSpartan-Regular.ttf", 32);
170    /// term.set_font("LeagueSpartan-Regular.ttf", 24);
171    /// ```
172    pub fn set_font(&mut self, font: &str, size: FontSize) {
173        if self.active {
174            if !self.art_mode { self.glyphs = load_font(&mut self.window, font); }
175            self.font = String::from(font);
176            self.font_size = size;
177        }
178    }
179
180    /// Loads a new art font from the given font filename and sets the given font size.
181    /// You probably want to use a mono-space font here, and a small size.
182    /// 
183    /// The default is LeagueMono-Regular.ttf at 10pt.
184    /// 
185    /// ```no_run
186    /// # use simpleterm::text::*;
187    /// # use simpleterm::terminal::Terminal;
188    /// # let mut term: Terminal = Terminal::new("simpleterm test", (800, 600), DARK_GREY, GOLD, "LeagueSpartan-Regular.ttf", 32);
189    /// term.set_art_font("LeagueMono-Regular.ttf", 10);
190    /// ```
191    pub fn set_art_font(&mut self, font: &str, size: FontSize) {
192        if self.active {
193            if self.art_mode { self.glyphs = load_font(&mut self.window, font); }
194            self.art_font = String::from(font);
195            self.art_font_size = size;
196        }
197    }
198
199    /// Changes the terminal's background and foreground to the given colors. The change will be apparent in the next text command.
200    /// 
201    /// ```no_run
202    /// # use simpleterm::text::*;
203    /// # use simpleterm::terminal::Terminal;
204    /// # let mut term: Terminal = Terminal::new("simpleterm test", (800, 600), DARK_GREY, GOLD, "LeagueSpartan-Regular.ttf", 32);
205    /// term.set_colors(DARK_GREY, CRIMSON);
206    /// ```
207    pub fn set_colors(&mut self, bgc: Color, fgc: Color) {
208        self.bg_color = bgc;
209        self.fg_color = fgc;
210    }
211
212    // Displays an art string along with the rest of the terminal.
213    fn show_art(&mut self, timer: Duration) {
214        let bgc: Color = self.bg_color;
215        let fgc: Color = self.fg_color;
216
217        let art: &Vec<String> = &self.message;
218        let glyphs: &mut Glyphs = &mut self.glyphs;
219        let font_size: FontSize = self.art_font_size;
220        let use_filter: bool = self.scanlines;
221        
222        let start: Instant = Instant::now();
223        let mut active: bool = self.active;
224        while let Some(e) = self.window.next() {
225            e.close(|_| { active = false; });
226
227            let win_size: Size = self.window.window.size();
228
229            let now: Instant = Instant::now();
230            if now.duration_since(start) > timer { break; }
231
232            self.window.draw_2d(&e, |c, g, device| {
233                clear(bgc, g);
234
235                draw_background(win_size, bgc, fgc, use_filter, c, g);
236                draw_art(win_size, art, glyphs, font_size, fgc, c, g);
237                draw_foreground(win_size, bgc, use_filter, c, g);
238            
239                glyphs.factory.encoder.flush(device);
240            });
241        }
242        self.active = active;
243    }
244
245    // Types a message one character at a time, waiting TYPE_TIME between each character.
246    fn type_message(&mut self) {
247        let bgc: Color = self.bg_color;
248        let fgc: Color = self.fg_color;
249        let current_input: &str = &(self.input[..]);
250        let glyphs = &mut self.glyphs;
251        let font_size: FontSize = self.font_size;
252
253        let mut typed_message: Vec<String> = Vec::new();
254        let use_filter: bool = self.scanlines;
255
256        let mut active: bool = self.active;
257        for (i, line) in self.message.iter().enumerate() {
258            typed_message.push(String::default());
259
260            let line_len: usize = line.len();
261            for j in 1..line_len {
262                typed_message[i] = String::from(&line[..=j]);
263                typed_message[i].push_str("[]");
264                if let Some(e) = self.window.next() {
265                    e.close(|_| { active = false; });
266
267                    let win_size: Size = self.window.window.size();
268
269                    self.window.draw_2d(&e, |c, g, device| {
270                        clear(bgc, g);
271
272                        draw_background(win_size, bgc, fgc, use_filter, c, g);
273                        draw_message(&typed_message, glyphs, font_size, fgc, c, g);
274                        draw_input(win_size, current_input, glyphs, font_size, fgc, c, g);
275                        draw_foreground(win_size, bgc, use_filter, c, g);
276                    
277                        glyphs.factory.encoder.flush(device);
278                    });
279                    thread::sleep(TYPE_TIME);
280                }
281                typed_message[i].pop();
282                typed_message[i].pop();
283            }
284        }
285        self.active = active;
286    }
287
288    // Displays the current terminal until the user presses Enter.
289    fn wait_for_continue(&mut self) {
290        let mut ready: bool = false;
291
292        let bgc: Color = self.bg_color;
293        let fgc: Color = self.fg_color;
294
295        let message: &Vec<String> = &self.message;
296        let current_input: &str = &(self.input);
297        let glyphs: &mut Glyphs = &mut self.glyphs;
298        let font_size: FontSize = self.font_size;
299        let use_filter: bool = self.scanlines;
300        
301        let mut start: Instant = Instant::now();
302        let mut active: bool = self.active;
303        while let Some(e) = self.window.next() {
304            e.close(|_| { active = false; });
305
306            let win_size: Size = self.window.window.size();
307
308            e.button(|button_args| {
309                if let Button::Keyboard(key) = button_args.button {
310                    if button_args.state == ButtonState::Press && key == Key::Return { ready = true; }
311                }
312            });
313
314            if ready { break; }
315
316            let now: Instant = Instant::now();
317            self.window.draw_2d(&e, |c, g, device| {
318                clear(bgc, g);
319
320                draw_background(win_size, bgc, fgc, use_filter, c, g);
321                draw_message(message, glyphs, font_size, fgc, c, g);
322                draw_input_marker(win_size, glyphs, font_size, fgc, c, g);
323                if check_flash(now, &mut start) { draw_input(win_size, current_input, glyphs, font_size, fgc, c, g); }
324                draw_foreground(win_size, bgc, use_filter, c, g);
325            
326                glyphs.factory.encoder.flush(device);
327            });
328        }
329        self.active = active;
330    }
331
332    // Displays the current terminal until the user submits some input.
333    fn wait_for_input(&mut self) {
334        let mut input_string: String = String::default();
335        let mut input_accepted: bool = false;
336
337        let bgc: Color = self.bg_color;
338        let fgc: Color = self.fg_color;
339
340        let message: &Vec<String> = &self.message;
341        let glyphs: &mut Glyphs = &mut self.glyphs;
342        let font_size: FontSize = self.font_size;
343        let use_filter: bool = self.scanlines;
344        
345        let mut start: Instant = Instant::now();
346        let mut active: bool = self.active;
347        while let Some(e) = self.window.next() {
348            e.close(|_| { active = false; });
349
350            let win_size: Size = self.window.window.size();
351            
352            e.text(|text| input_string.push_str(text));
353            e.button(|button_args| {
354                if let Button::Keyboard(key) = button_args.button {
355                    if button_args.state == ButtonState::Press {
356                        if key == Key::Backspace { input_string.pop(); }
357                        if key == Key::Return && input_string != "" { input_accepted = true; }
358                    }
359                }
360            });
361
362            if input_accepted {
363                self.input = input_string.clone();
364                input_string = String::default();
365            }
366            
367            let now: Instant = Instant::now();
368            self.window.draw_2d(&e, |c, g, device| {
369                clear(bgc, g);
370
371                draw_background(win_size, bgc, fgc, use_filter, c, g);
372                draw_message(message, glyphs, font_size, fgc, c, g);
373                draw_input_marker(win_size, glyphs, font_size, fgc, c, g);
374
375                if check_flash(now, &mut start) {
376                    input_string.push_str("[]");
377                    draw_input(win_size, &input_string[..], glyphs, font_size, fgc, c, g);
378                    input_string.pop();
379                    input_string.pop();
380                } else {
381                    draw_input(win_size, &input_string[..], glyphs, font_size, fgc, c, g);
382                }
383                
384                draw_foreground(win_size, bgc, use_filter, c, g);
385            
386                glyphs.factory.encoder.flush(device);
387            });
388
389            if input_accepted { break; }
390        }
391        self.active = active;
392    }
393
394    // Displays an the current terminal until the timer runs out.
395    fn wait_for_timer(&mut self, timer: Duration) {
396        let bgc: Color = self.bg_color;
397        let fgc: Color = self.fg_color;
398
399        let message: &Vec<String> = &self.message;
400        let glyphs: &mut Glyphs = &mut self.glyphs;
401        let font_size: FontSize = self.font_size;
402        let use_filter: bool = self.scanlines;
403        
404        let start: Instant = Instant::now();
405        let mut active: bool = self.active;
406        while let Some(e) = self.window.next() {
407            e.close(|_| { active = false; });
408
409            let win_size: Size = self.window.window.size();
410
411            let now: Instant = Instant::now();
412            if now.duration_since(start) > timer { break; }
413
414            self.window.draw_2d(&e, |c, g, device| {
415                clear(bgc, g);
416
417                draw_background(win_size, bgc, fgc, use_filter, c, g);
418                draw_message(message, glyphs, font_size, fgc, c, g);
419                draw_foreground(win_size, bgc, use_filter, c, g);
420            
421                glyphs.factory.encoder.flush(device);
422            });
423        }
424        self.active = active;
425    }
426
427    // Processes a new message and types it out.
428    fn new_message(&mut self, message: &str) {
429        self.message = message.split('\n').map(String::from).collect();
430        self.process_message();
431        self.input = String::default();
432        self.type_message();
433    }
434
435    // Splits a message into a vector of strings that can fit in the current window's bounds.
436    fn process_message(&mut self) {
437        let max_chars: usize = self.get_max_characters();
438
439        let mut new_message_vec: Vec<String> = Vec::new();
440
441        for old_message in self.message.iter() {
442            let mut new_message: String = String::new();
443
444            for word in old_message.split_whitespace() {
445                let word_len: usize = word.len();
446                let message_len: usize = new_message.len();
447
448                if word_len > max_chars {
449                    if message_len > 0 {
450                        let word_vec = split_word(word, max_chars - (message_len + 1), max_chars);
451                        let mut word_iter = word_vec.iter();
452                        new_message_vec.push(format!("{} {}", new_message, word_iter.next().unwrap()));
453                        for continued_word in word_iter {
454                            new_message_vec.push(continued_word.to_string());
455                        }
456                        new_message = new_message_vec.pop().unwrap();
457                    } else {
458                        new_message_vec.append(&mut split_word(word, max_chars, max_chars));
459                    }
460                } else if message_len + word_len > max_chars {
461                    new_message_vec.push(new_message);
462                    new_message = String::from(word);
463                } else if message_len > 0 {
464                    new_message = format!("{} {}", new_message, word);
465                } else {
466                    new_message = String::from(word);
467                }
468            }
469            if !new_message.is_empty() { new_message_vec.push(new_message); }
470        }
471        self.message = new_message_vec;
472    }
473
474    // Determines the max number of characters based on window and font size.
475    fn get_max_characters(&self) -> usize {
476        ((self.window.window.size().width / self.font_size as f64) * 2.15) as usize
477    }
478}