doryen_rs/
console.rs

1use std::collections::HashMap;
2
3use crate::color::{color_blend, Color};
4
5// rectangle drawing kit
6pub const CHAR_CORNER_NW: u16 = 218;
7pub const CHAR_CORNER_SW: u16 = 192;
8pub const CHAR_CORNER_SE: u16 = 217;
9pub const CHAR_CORNER_NE: u16 = 191;
10pub const CHAR_LINE_H: u16 = 196;
11pub const CHAR_LINE_V: u16 = 179;
12// sub-pixel resolution kit
13pub const CHAR_SUBP_NW: u16 = 226;
14pub const CHAR_SUBP_NE: u16 = 227;
15pub const CHAR_SUBP_N: u16 = 228;
16pub const CHAR_SUBP_SE: u16 = 229;
17pub const CHAR_SUBP_DIAG: u16 = 230;
18pub const CHAR_SUBP_E: u16 = 231;
19pub const CHAR_SUBP_SW: u16 = 232;
20
21#[derive(Copy, Clone)]
22pub enum TextAlign {
23    Left,
24    Right,
25    Center,
26}
27
28/// This contains the data for a console (including the one displayed on the screen) and methods to draw on it.
29pub struct Console {
30    width: u32,
31    height: u32,
32    // power of 2 size (for textures)
33    pot_width: u32,
34    pot_height: u32,
35    ascii: Vec<u32>,
36    back: Vec<Color>,
37    fore: Vec<Color>,
38    colors: HashMap<String, Color>,
39    color_stack: Vec<Color>,
40}
41
42impl Console {
43    /// create a new offscreen console that you can blit on another console
44    /// width and height are in cells (characters), not pixels.
45    pub fn new(width: u32, height: u32) -> Self {
46        let mut back = Vec::new();
47        let mut fore = Vec::new();
48        let mut ascii = Vec::new();
49        let mut pot_width = 1;
50        let mut pot_height = 1;
51        while pot_width < width {
52            pot_width *= 2;
53        }
54        while pot_height < height {
55            pot_height *= 2;
56        }
57        for _ in 0..(pot_width * pot_height) as usize {
58            back.push((0, 0, 0, 255));
59            fore.push((255, 255, 255, 255));
60            ascii.push(' ' as u32);
61        }
62        Self {
63            width,
64            height,
65            ascii,
66            back,
67            fore,
68            pot_width,
69            pot_height,
70            colors: HashMap::new(),
71            color_stack: Vec::new(),
72        }
73    }
74    /// resizes the console
75    pub fn resize(&mut self, width: u32, height: u32) {
76        self.width = width;
77        self.height = height;
78        let mut pot_width = 1;
79        let mut pot_height = 1;
80        while pot_width < width {
81            pot_width *= 2;
82        }
83        while pot_height < height {
84            pot_height *= 2;
85        }
86        self.pot_height = pot_height;
87        self.pot_width = pot_width;
88        self.back.clear();
89        self.fore.clear();
90        self.ascii.clear();
91        for _ in 0..(pot_width * pot_height) as usize {
92            self.back.push((0, 0, 0, 255));
93            self.fore.push((255, 255, 255, 255));
94            self.ascii.push(' ' as u32);
95        }
96    }
97    /// associate a name with a color for this console.
98    /// The color name can then be used in [`Console::print_color`]
99    /// Example
100    /// ```
101    /// use doryen_rs::{Console, TextAlign};
102    /// let mut con=Console::new(80,25);
103    /// con.register_color("pink", (255, 0, 255, 255));
104    /// con.print_color(5, 5, "This text contains a #[pink]pink#[] word", TextAlign::Left, None);
105    /// ```
106    pub fn register_color(&mut self, name: &str, value: Color) {
107        self.colors.insert(name.to_owned(), value);
108    }
109    pub fn get_width(&self) -> u32 {
110        self.width
111    }
112    pub fn get_height(&self) -> u32 {
113        self.height
114    }
115    pub fn get_size(&self) -> (u32, u32) {
116        (self.width, self.height)
117    }
118    pub fn get_pot_width(&self) -> u32 {
119        self.pot_width
120    }
121    pub fn get_pot_height(&self) -> u32 {
122        self.pot_height
123    }
124    /// for fast reading of the characters values
125    pub fn borrow_ascii(&self) -> &Vec<u32> {
126        &self.ascii
127    }
128    /// for fast reading of the characters colors
129    pub fn borrow_foreground(&self) -> &Vec<Color> {
130        &self.fore
131    }
132    /// for fast reading of the background colors
133    pub fn borrow_background(&self) -> &Vec<Color> {
134        &self.back
135    }
136    /// for fast writing of the characters values
137    pub fn borrow_mut_ascii(&mut self) -> &mut Vec<u32> {
138        &mut self.ascii
139    }
140    /// for fast writing of the characters colors
141    pub fn borrow_mut_foreground(&mut self) -> &mut Vec<Color> {
142        &mut self.fore
143    }
144    /// for fast writing of the background colors
145    pub fn borrow_mut_background(&mut self) -> &mut Vec<Color> {
146        &mut self.back
147    }
148    /// get the background color of a cell (if x,y inside the console)
149    pub fn get_back(&self, x: i32, y: i32) -> Option<Color> {
150        if self.check_coords(x, y) {
151            return Some(self.unsafe_get_back(x, y));
152        }
153        None
154    }
155    /// get the foreground color of a cell (if x,y inside the console)
156    pub fn get_fore(&self, x: i32, y: i32) -> Option<Color> {
157        if self.check_coords(x, y) {
158            return Some(self.unsafe_get_fore(x, y));
159        }
160        None
161    }
162    /// get the ascii code of a cell (if x,y inside the console)
163    pub fn get_ascii(&self, x: i32, y: i32) -> Option<u16> {
164        if self.check_coords(x, y) {
165            return Some(self.unsafe_get_ascii(x, y));
166        }
167        None
168    }
169    /// get the background color of a cell (no boundary check)
170    pub fn unsafe_get_back(&self, x: i32, y: i32) -> Color {
171        let off = self.offset(x, y);
172        self.back[off]
173    }
174    /// get the foreground color of a cell (no boundary check)
175    pub fn unsafe_get_fore(&self, x: i32, y: i32) -> Color {
176        let off = self.offset(x, y);
177        self.fore[off]
178    }
179    /// get the ascii code of a cell (no boundary check)
180    pub fn unsafe_get_ascii(&self, x: i32, y: i32) -> u16 {
181        let off = self.offset(x, y);
182        self.ascii[off] as u16
183    }
184    fn offset(&self, x: i32, y: i32) -> usize {
185        x as usize + y as usize * self.pot_width as usize
186    }
187    fn check_coords(&self, x: i32, y: i32) -> bool {
188        (x as u32) < self.width && (y as u32) < self.height
189    }
190    /// set the character at a specific position (doesn't change the color).
191    ///
192    /// Since the glyph associated with an ascii code depends on the font you're using,
193    /// doryen-rs can't provide constants for specific characters except for a few ones used internally.
194    ///
195    /// More information about this [here](https://github.com/jice-nospam/doryen-rs/issues/7).
196    ///
197    /// You can find some constants that work with most fonts in [this file](https://github.com/tomassedovic/tcod-rs/blob/master/src/chars.rs) provided by Alex Mooney.
198    pub fn ascii(&mut self, x: i32, y: i32, ascii: u16) {
199        if self.check_coords(x, y) {
200            self.unsafe_ascii(x, y, ascii);
201        }
202    }
203    /// set the character color at a specific position
204    pub fn fore(&mut self, x: i32, y: i32, col: Color) {
205        if self.check_coords(x, y) {
206            self.unsafe_fore(x, y, col);
207        }
208    }
209    /// set the background color at a specific position
210    pub fn back(&mut self, x: i32, y: i32, col: Color) {
211        if self.check_coords(x, y) {
212            self.unsafe_back(x, y, col);
213        }
214    }
215    /// set the character at a specific position (no boundary check)
216    pub fn unsafe_ascii(&mut self, x: i32, y: i32, ascii: u16) {
217        let off = self.offset(x, y);
218        self.ascii[off] = u32::from(ascii);
219    }
220    /// set the character color at a specific position (no boundary check)
221    pub fn unsafe_fore(&mut self, x: i32, y: i32, col: Color) {
222        let off = self.offset(x, y);
223        self.fore[off] = col;
224    }
225    /// set the background color at a specific position (no boundary check)
226    pub fn unsafe_back(&mut self, x: i32, y: i32, col: Color) {
227        let off = self.offset(x, y);
228        self.back[off] = col;
229    }
230    /// fill the whole console with values
231    pub fn clear(&mut self, fore: Option<Color>, back: Option<Color>, fillchar: Option<u16>) {
232        let w = self.width;
233        let h = self.height;
234        self.area(0, 0, w, h, fore, back, fillchar);
235    }
236    /// write a multi-color string. Foreground color is defined by #[color_name] patterns inside the string.
237    /// color_name must have been registered with [`Console::register_color`] before.
238    /// Default foreground color is white, at the start of the string.
239    /// When an unknown color name is used, the color goes back to its previous value.
240    /// You can then use an empty name to end a color span.
241    /// Example
242    /// ```
243    /// use doryen_rs::{Console, TextAlign};
244    /// let mut con=Console::new(80,25);
245    /// con.register_color("pink", (255, 0, 255, 255));
246    /// con.register_color("blue", (0, 0, 255, 255));
247    /// con.print_color(5, 5, "#[blue]This blue text contains a #[pink]pink#[] word", TextAlign::Left, None);
248    /// ```
249    pub fn print_color(
250        &mut self,
251        x: i32,
252        y: i32,
253        text: &str,
254        align: TextAlign,
255        back: Option<Color>,
256    ) {
257        let mut cury = y;
258        self.color_stack.clear();
259        for line in text.to_owned().split('\n') {
260            self.print_line_color(x, cury, line, align, back);
261            cury += 1;
262        }
263    }
264
265    /// compute the length of a string containing color codes.
266    /// Example :
267    /// ```
268    /// use doryen_rs::Console;
269    /// let len = Console::text_color_len("#[red]red text with a #[blue]blue#[] word");
270    /// assert_eq!(len, 25); // actual string : "red text with a blue word"
271    /// let len = Console::text_color_len("#[red]a\nb");
272    /// assert_eq!(len, 3); // actual string : "a\nb"
273    /// let len = Console::text_color_len("normal string");
274    /// assert_eq!(len, 13);
275    /// ```
276    pub fn text_color_len(text: &str) -> usize {
277        let mut text_len = 0;
278        for color_span in text.to_owned().split("#[") {
279            if color_span.is_empty() {
280                continue;
281            }
282            let mut col_text = color_span.split(']');
283            let col_name = col_text.next().unwrap();
284            if let Some(text_span) = col_text.next() {
285                text_len += text_span.chars().count();
286            } else {
287                text_len += col_name.chars().count();
288            }
289        }
290        text_len
291    }
292
293    fn get_color_spans(&mut self, text: &str, text_len: &mut i32) -> Vec<(Color, String)> {
294        let mut spans: Vec<(Color, String)> = Vec::new();
295        *text_len = 0;
296        let mut fore = *self.color_stack.last().unwrap_or(&(255, 255, 255, 255));
297        for color_span in text.to_owned().split("#[") {
298            if color_span.is_empty() {
299                continue;
300            }
301            let mut col_text = color_span.splitn(2, ']');
302            let col_name = col_text.next().unwrap();
303            if let Some(text_span) = col_text.next() {
304                if let Some(color) = self.colors.get(col_name) {
305                    fore = *color;
306                    self.color_stack.push(fore);
307                } else {
308                    self.color_stack.pop();
309                    fore = *self.color_stack.last().unwrap_or(&(255, 255, 255, 255));
310                }
311                spans.push((fore, text_span.to_owned()));
312                *text_len += text_span.chars().count() as i32;
313            } else {
314                spans.push((fore, col_name.to_owned()));
315                *text_len += col_name.chars().count() as i32;
316            }
317        }
318        spans
319    }
320
321    fn print_line_color(
322        &mut self,
323        x: i32,
324        y: i32,
325        text: &str,
326        align: TextAlign,
327        back: Option<Color>,
328    ) {
329        let mut str_len = 0;
330        let spans = self.get_color_spans(text, &mut str_len);
331        let mut ix = match align {
332            TextAlign::Left => x,
333            TextAlign::Right => x - str_len + 1,
334            TextAlign::Center => x - str_len / 2,
335        };
336        for (color, span) in spans {
337            self.print_line(ix, y, &span, TextAlign::Left, Some(color), back);
338            ix += span.chars().count() as i32;
339        }
340    }
341    /// write a string. If the string reaches the border of the console, it's truncated.
342    /// If the string contains carriage return `"\n"`, multiple lines are printed.
343    pub fn print(
344        &mut self,
345        x: i32,
346        y: i32,
347        text: &str,
348        align: TextAlign,
349        fore: Option<Color>,
350        back: Option<Color>,
351    ) {
352        let mut cury = y;
353        for line in text.to_owned().split('\n') {
354            self.print_line(x, cury, line, align, fore, back);
355            cury += 1;
356        }
357    }
358    fn print_line(
359        &mut self,
360        x: i32,
361        y: i32,
362        text: &str,
363        align: TextAlign,
364        fore: Option<Color>,
365        back: Option<Color>,
366    ) {
367        let stext = text.to_owned();
368        let mut str_len = stext.chars().count() as i32;
369        let mut start = 0;
370        let mut ix = match align {
371            TextAlign::Left => x,
372            TextAlign::Right => x - str_len + 1,
373            TextAlign::Center => x - str_len / 2,
374        };
375        if ix < 0 {
376            str_len += ix;
377            start -= ix;
378            ix = 0;
379        }
380        if ix + str_len > self.width as i32 {
381            str_len = self.width as i32 - ix;
382        }
383        let mut chars = stext.chars().skip(start as usize);
384        for _ in 0..str_len {
385            let ch = chars.next();
386            self.cell(ix, y, Some(ch.unwrap() as u16), fore, back);
387            ix += 1;
388        }
389    }
390    /// draw a rectangle, possibly filling it with a character.
391    pub fn rectangle(
392        &mut self,
393        x: i32,
394        y: i32,
395        w: u32,
396        h: u32,
397        fore: Option<Color>,
398        back: Option<Color>,
399        fill: Option<u16>,
400    ) {
401        let right = x + (w as i32) - 1;
402        let down = y + (h as i32) - 1;
403        self.cell(x, y, Some(CHAR_CORNER_NW), fore, back);
404        self.cell(right, down, Some(CHAR_CORNER_SE), fore, back);
405        self.cell(right, y, Some(CHAR_CORNER_NE), fore, back);
406        self.cell(x, down, Some(CHAR_CORNER_SW), fore, back);
407        if (y as u32) < self.height {
408            self.area(x + 1, y, w - 2, 1, fore, back, Some(CHAR_LINE_H));
409        }
410        if (down as u32) < self.height {
411            self.area(x + 1, down, w - 2, 1, fore, back, Some(CHAR_LINE_H));
412        }
413        if (x as u32) < self.width {
414            self.area(x, y + 1, 1, h - 2, fore, back, Some(CHAR_LINE_V));
415        }
416        if (right as u32) < self.width {
417            self.area(right, y + 1, 1, h - 2, fore, back, Some(CHAR_LINE_V));
418        }
419        if fill.is_some() {
420            self.area(x + 1, y + 1, w - 2, h - 2, fore, back, fill);
421        }
422    }
423    /// fill an area with values
424    pub fn area(
425        &mut self,
426        x: i32,
427        y: i32,
428        w: u32,
429        h: u32,
430        fore: Option<Color>,
431        back: Option<Color>,
432        fillchar: Option<u16>,
433    ) {
434        let right = x + (w as i32);
435        let down = y + (h as i32);
436        if let Some(fillchar) = fillchar {
437            for iy in y.max(0)..down.min(self.height as i32) {
438                let off = iy * self.pot_width as i32;
439                for ix in x.max(0)..right.min(self.width as i32) {
440                    self.ascii[(off + ix) as usize] = u32::from(fillchar);
441                }
442            }
443        }
444        if let Some(fore) = fore {
445            for iy in y.max(0)..down.min(self.height as i32) {
446                let off = iy * self.pot_width as i32;
447                for ix in x.max(0)..right.min(self.width as i32) {
448                    self.fore[(off + ix) as usize] = fore;
449                }
450            }
451        }
452        if let Some(back) = back {
453            for iy in y.max(0)..down.min(self.height as i32) {
454                let off = iy * self.pot_width as i32;
455                for ix in x.max(0)..right.min(self.width as i32) {
456                    self.back[(off + ix) as usize] = back;
457                }
458            }
459        }
460    }
461    /// can change all properties of a console cell at once
462    pub fn cell(
463        &mut self,
464        x: i32,
465        y: i32,
466        ascii: Option<u16>,
467        fore: Option<Color>,
468        back: Option<Color>,
469    ) {
470        if self.check_coords(x, y) {
471            let off = self.offset(x, y);
472            if let Some(ascii) = ascii {
473                self.ascii[off] = u32::from(ascii);
474            }
475            if let Some(fore) = fore {
476                self.fore[off] = fore;
477            }
478            if let Some(back) = back {
479                self.back[off] = back;
480            }
481        }
482    }
483    /// blit (draw) a console onto another one
484    /// You can use fore_alpha and back_alpha to blend this console with existing background on the destination.
485    /// If you define a key color, the cells using this color as background will be ignored. This makes it possible to blit
486    /// non rectangular zones.
487    pub fn blit(
488        &self,
489        x: i32,
490        y: i32,
491        destination: &mut Console,
492        fore_alpha: f32,
493        back_alpha: f32,
494        key_color: Option<Color>,
495    ) {
496        self.blit_ex(
497            0,
498            0,
499            self.width as i32,
500            self.height as i32,
501            destination,
502            x,
503            y,
504            fore_alpha,
505            back_alpha,
506            key_color,
507        );
508    }
509    /// blit a region of this console onto another one.
510    /// see [`Console::blit`]
511    pub fn blit_ex(
512        &self,
513        xsrc: i32,
514        ysrc: i32,
515        wsrc: i32,
516        hsrc: i32,
517        destination: &mut Console,
518        xdst: i32,
519        ydst: i32,
520        fore_alpha: f32,
521        back_alpha: f32,
522        key_color: Option<Color>,
523    ) {
524        for y in 0..hsrc - ysrc {
525            let off = (y + ysrc) * self.pot_width as i32;
526            let doff = (y + ydst) * destination.pot_width as i32;
527            for x in 0..wsrc - xsrc {
528                if self.check_coords(xsrc + x, ysrc + y)
529                    && destination.check_coords(xdst + x, ydst + y)
530                {
531                    let src_idx = (off + x + xsrc) as usize;
532                    let dest_idx = (doff + x + xdst) as usize;
533                    let src_back = self.back[src_idx];
534                    let dst_back = destination.back[dest_idx];
535                    if back_alpha > 0.0 {
536                        let back = self.back[src_idx];
537                        if let Some(key) = key_color {
538                            if key == back {
539                                continue;
540                            }
541                        }
542                        destination.back[dest_idx] = color_blend(dst_back, src_back, back_alpha);
543                    }
544                    if fore_alpha > 0.0 {
545                        let src_fore = self.fore[src_idx];
546                        let dst_fore = destination.fore[dest_idx];
547                        let src_char = self.ascii[src_idx];
548                        let dst_char = destination.ascii[dest_idx];
549                        let dst_back = destination.back[dest_idx];
550                        if fore_alpha < 1.0 {
551                            if src_char == ' ' as u32 {
552                                destination.fore[dest_idx] =
553                                    color_blend(dst_fore, src_back, back_alpha);
554                            } else if dst_char == ' ' as u32 {
555                                destination.ascii[dest_idx] = src_char;
556                                destination.fore[dest_idx] =
557                                    color_blend(dst_back, src_fore, fore_alpha);
558                            } else if dst_char == src_char {
559                                destination.fore[dest_idx] =
560                                    color_blend(dst_fore, src_fore, fore_alpha);
561                            } else if fore_alpha < 0.5 {
562                                destination.fore[dest_idx] =
563                                    color_blend(dst_fore, dst_back, fore_alpha * 2.0);
564                            } else {
565                                destination.ascii[dest_idx] = src_char;
566                                destination.fore[dest_idx] =
567                                    color_blend(dst_back, src_fore, (fore_alpha - 0.5) * 2.0);
568                            }
569                        } else {
570                            destination.fore[dest_idx] = src_fore;
571                            destination.ascii[dest_idx] = src_char;
572                        }
573                    }
574                }
575            }
576        }
577    }
578}