bracket_terminal/consoles/
flexible_console.rs

1use crate::prelude::{
2    string_to_cp437, to_cp437, CharacterTranslationMode, ColoredTextSpans, Console, FontCharType,
3    TextAlign, XpLayer,
4};
5use bracket_color::prelude::RGBA;
6use bracket_geometry::prelude::{PointF, Rect};
7use bracket_rex::prelude::XpColor;
8use ultraviolet::Vec2;
9
10use std::any::Any;
11
12/// Internal storage structure for sparse tiles.
13pub struct FlexiTile {
14    pub position: PointF,
15    pub z_order: i32,
16    pub glyph: FontCharType,
17    pub fg: RGBA,
18    pub bg: RGBA,
19    pub rotation: f32,
20    pub scale: PointF,
21}
22
23/// A sparse console. Rather than storing every cell on the screen, it stores just cells that have
24/// data.
25pub struct FlexiConsole {
26    pub width: u32,
27    pub height: u32,
28
29    pub tiles: Vec<FlexiTile>,
30    pub is_dirty: bool,
31
32    // To handle offset tiles for people who want thin walls between tiles
33    pub offset_x: f32,
34    pub offset_y: f32,
35
36    pub scale: f32,
37    pub scale_center: (i32, i32),
38
39    pub extra_clipping: Option<Rect>,
40    pub translation: CharacterTranslationMode,
41    pub(crate) needs_resize_internal: bool,
42}
43
44impl FlexiConsole {
45    /// Initializes the console.
46    pub fn init(width: u32, height: u32) -> Box<FlexiConsole> {
47        // Console backing initialization
48        let new_console = FlexiConsole {
49            width,
50            height,
51            tiles: Vec::with_capacity((width * height) as usize),
52            is_dirty: true,
53            offset_x: 0.0,
54            offset_y: 0.0,
55            scale: 1.0,
56            scale_center: (width as i32 / 2, height as i32 / 2),
57            extra_clipping: None,
58            translation: CharacterTranslationMode::Codepage437,
59            needs_resize_internal: false,
60        };
61
62        Box::new(new_console)
63    }
64
65    // Insert a single tile with "fancy" attributes
66    #[allow(clippy::too_many_arguments)]
67    pub fn set_fancy(
68        &mut self,
69        position: PointF,
70        z_order: i32,
71        rotation: f32,
72        scale: PointF,
73        fg: RGBA,
74        bg: RGBA,
75        glyph: FontCharType,
76    ) {
77        self.is_dirty = true;
78        let invert_pos = PointF {
79            x: position.x,
80            y: self.height as f32 - position.y,
81        };
82        self.tiles.push(FlexiTile {
83            position: invert_pos,
84            z_order,
85            glyph,
86            fg,
87            bg,
88            rotation,
89            scale,
90        });
91    }
92}
93
94impl Console for FlexiConsole {
95    fn get_char_size(&self) -> (u32, u32) {
96        (self.width, self.height)
97    }
98
99    fn resize_pixels(&mut self, _width: u32, _height: u32) {
100        self.is_dirty = true;
101    }
102
103    /// Translates x/y to an index entry. Not really useful.
104    fn at(&self, x: i32, y: i32) -> usize {
105        (((self.height - 1 - y as u32) * self.width) + x as u32) as usize
106    }
107
108    /// Clear the screen.
109    fn cls(&mut self) {
110        self.is_dirty = true;
111        self.tiles.clear();
112
113        self.tiles.push(FlexiTile {
114            glyph: 32,
115            fg: RGBA::from_u8(255, 255, 255, 255),
116            bg: RGBA::from_u8(0, 0, 0, 255),
117            rotation: 0.,
118            scale: Vec2::new(0., 0.),
119            z_order: 0,
120            position: Vec2::new(0., 0.),
121        });
122    }
123
124    /// Clear the screen. Since we don't HAVE a background, it doesn't use it.
125    fn cls_bg(&mut self, _background: RGBA) {
126        self.is_dirty = true;
127        for tile in &mut self.tiles {
128            tile.glyph = 32;
129            tile.fg = RGBA::from_u8(255, 255, 255, 255);
130            tile.bg = RGBA::from_u8(0, 0, 0, 255);
131        }
132    }
133
134    /// Prints a string to an x/y position.
135    fn print(&mut self, x: i32, y: i32, output: &str) {
136        self.is_dirty = true;
137
138        let bytes = match self.translation {
139            CharacterTranslationMode::Codepage437 => string_to_cp437(output),
140            CharacterTranslationMode::Unicode => {
141                output.chars().map(|c| c as FontCharType).collect()
142            }
143        };
144
145        let h = (self.height - 1) as f32;
146        self.tiles
147            .extend(bytes.into_iter().enumerate().map(|(i, glyph)| FlexiTile {
148                position: PointF {
149                    x: i as f32 + x as f32,
150                    y: h - y as f32,
151                },
152                z_order: 0,
153                glyph,
154                fg: RGBA::from_f32(1.0, 1.0, 1.0, 1.0),
155                bg: RGBA::from_f32(0.0, 0.0, 0.0, 1.0),
156                rotation: 0.0,
157                scale: PointF { x: 1.0, y: 1.0 },
158            }));
159    }
160
161    /// Prints a string to an x/y position, with foreground and background colors.
162    fn print_color(&mut self, x: i32, y: i32, fg: RGBA, bg: RGBA, output: &str) {
163        self.is_dirty = true;
164
165        let bytes = match self.translation {
166            CharacterTranslationMode::Codepage437 => string_to_cp437(output),
167            CharacterTranslationMode::Unicode => {
168                output.chars().map(|c| c as FontCharType).collect()
169            }
170        };
171        let h = (self.height - 1) as f32;
172        self.tiles
173            .extend(bytes.into_iter().enumerate().map(|(i, glyph)| FlexiTile {
174                z_order: 0,
175                position: PointF {
176                    x: i as f32 + x as f32,
177                    y: h - y as f32,
178                },
179                glyph,
180                fg,
181                bg,
182                rotation: 0.0,
183                scale: PointF { x: 1.0, y: 1.0 },
184            }));
185    }
186
187    /// Sets a single cell in the console
188    fn set(&mut self, x: i32, y: i32, fg: RGBA, bg: RGBA, glyph: FontCharType) {
189        self.is_dirty = true;
190        if self.try_at(x, y).is_some() {
191            let h = (self.height - 1) as f32;
192            self.tiles.push(FlexiTile {
193                position: PointF {
194                    x: x as f32,
195                    y: h - y as f32,
196                },
197                z_order: 0,
198                glyph,
199                fg,
200                bg,
201                rotation: 0.0,
202                scale: PointF { x: 1.0, y: 1.0 },
203            });
204        }
205    }
206
207    /// Sets a single cell in the console's background
208    fn set_bg(&mut self, _x: i32, _y: i32, _bg: RGBA) {
209        // Does nothing for this layer type
210    }
211
212    /// Draws a box, starting at x/y with the extents width/height using CP437 line characters
213    fn draw_box(&mut self, sx: i32, sy: i32, width: i32, height: i32, fg: RGBA, bg: RGBA) {
214        crate::prelude::draw_box(self, sx, sy, width, height, fg, bg);
215    }
216
217    /// Draws a box, starting at x/y with the extents width/height using CP437 double line characters
218    fn draw_box_double(&mut self, sx: i32, sy: i32, width: i32, height: i32, fg: RGBA, bg: RGBA) {
219        crate::prelude::draw_box_double(self, sx, sy, width, height, fg, bg);
220    }
221
222    /// Draws a box, starting at x/y with the extents width/height using CP437 line characters
223    fn draw_hollow_box(&mut self, sx: i32, sy: i32, width: i32, height: i32, fg: RGBA, bg: RGBA) {
224        crate::prelude::draw_hollow_box(self, sx, sy, width, height, fg, bg);
225    }
226
227    /// Draws a box, starting at x/y with the extents width/height using CP437 double line characters
228    fn draw_hollow_box_double(
229        &mut self,
230        sx: i32,
231        sy: i32,
232        width: i32,
233        height: i32,
234        fg: RGBA,
235        bg: RGBA,
236    ) {
237        crate::prelude::draw_hollow_box_double(self, sx, sy, width, height, fg, bg);
238    }
239
240    /// Fills a rectangle with the specified rendering information
241    fn fill_region(&mut self, target: Rect, glyph: FontCharType, fg: RGBA, bg: RGBA) {
242        target.for_each(|point| {
243            self.set(point.x, point.y, fg, bg, glyph);
244        });
245    }
246
247    /// Draws a horizontal progress bar
248    fn draw_bar_horizontal(
249        &mut self,
250        sx: i32,
251        sy: i32,
252        width: i32,
253        n: i32,
254        max: i32,
255        fg: RGBA,
256        bg: RGBA,
257    ) {
258        crate::prelude::draw_bar_horizontal(self, sx, sy, width, n, max, fg, bg);
259    }
260
261    /// Draws a vertical progress bar
262    fn draw_bar_vertical(
263        &mut self,
264        sx: i32,
265        sy: i32,
266        height: i32,
267        n: i32,
268        max: i32,
269        fg: RGBA,
270        bg: RGBA,
271    ) {
272        crate::prelude::draw_bar_vertical(self, sx, sy, height, n, max, fg, bg);
273    }
274
275    /// Prints text, centered to the whole console width, at vertical location y.
276    fn print_centered(&mut self, y: i32, text: &str) {
277        self.is_dirty = true;
278        self.print(
279            (self.width as i32 / 2) - (text.to_string().len() as i32 / 2),
280            y,
281            text,
282        );
283    }
284
285    /// Prints text in color, centered to the whole console width, at vertical location y.
286    fn print_color_centered(&mut self, y: i32, fg: RGBA, bg: RGBA, text: &str) {
287        self.is_dirty = true;
288        self.print_color(
289            (self.width as i32 / 2) - (text.to_string().len() as i32 / 2),
290            y,
291            fg,
292            bg,
293            text,
294        );
295    }
296
297    /// Prints text, centered to the whole console width, at vertical location y.
298    fn print_centered_at(&mut self, x: i32, y: i32, text: &str) {
299        self.is_dirty = true;
300        self.print(x - (text.to_string().len() as i32 / 2), y, text);
301    }
302
303    /// Prints text in color, centered to the whole console width, at vertical location y.
304    fn print_color_centered_at(&mut self, x: i32, y: i32, fg: RGBA, bg: RGBA, text: &str) {
305        self.is_dirty = true;
306        self.print_color(x - (text.to_string().len() as i32 / 2), y, fg, bg, text);
307    }
308
309    /// Prints text right-aligned
310    fn print_right(&mut self, x: i32, y: i32, text: &str) {
311        let len = text.len() as i32;
312        let actual_x = x - len;
313        self.print(actual_x, y, text);
314    }
315
316    /// Prints colored text right-aligned
317    fn print_color_right(&mut self, x: i32, y: i32, fg: RGBA, bg: RGBA, text: &str) {
318        let len = text.len() as i32;
319        let actual_x = x - len;
320        self.print_color(actual_x, y, fg, bg, text);
321    }
322
323    /// Print a colorized string with the color encoding defined inline.
324    /// For example: printer(1, 1, "#[blue]This blue text contains a #[pink]pink#[] word")
325    /// You can get the same effect with a TextBlock, but this can be easier.
326    /// Thanks to doryen_rs for the idea.
327    fn printer(
328        &mut self,
329        x: i32,
330        y: i32,
331        output: &str,
332        align: TextAlign,
333        background: Option<RGBA>,
334    ) {
335        let bg = if let Some(bg) = background {
336            bg
337        } else {
338            RGBA::from_u8(0, 0, 0, 255)
339        };
340
341        let split_text = ColoredTextSpans::new(output);
342
343        let mut tx = match align {
344            TextAlign::Left => x,
345            TextAlign::Center => x - (split_text.length as i32 / 2),
346            TextAlign::Right => x - split_text.length as i32,
347        };
348        for span in split_text.spans.iter() {
349            let fg = span.0;
350            for ch in span.1.chars() {
351                self.set(
352                    tx,
353                    y,
354                    fg,
355                    bg,
356                    match self.translation {
357                        CharacterTranslationMode::Codepage437 => to_cp437(ch),
358                        CharacterTranslationMode::Unicode => ch as FontCharType,
359                    },
360                );
361                tx += 1;
362            }
363        }
364    }
365
366    /// Saves the layer to an XpFile structure
367    fn to_xp_layer(&self) -> XpLayer {
368        let mut layer = XpLayer::new(self.width as usize, self.height as usize);
369
370        // Clear all to transparent
371        for y in 0..self.height {
372            for x in 0..self.width {
373                let cell = layer.get_mut(x as usize, y as usize).unwrap();
374                cell.bg = XpColor::TRANSPARENT;
375            }
376        }
377
378        for c in &self.tiles {
379            let x = c.position.x as usize;
380            let y = c.position.y as usize;
381            let cell = layer.get_mut(x as usize, y as usize).unwrap();
382            cell.ch = u32::from(c.glyph);
383            cell.fg = XpColor::from(c.fg);
384            cell.bg = XpColor::from(c.bg);
385        }
386
387        layer
388    }
389
390    /// Sets an offset to total console rendering, useful for layers that
391    /// draw between tiles. Offsets are specified as a percentage of total
392    /// character size; so -0.5 will offset half a character to the left/top.
393    fn set_offset(&mut self, x: f32, y: f32) {
394        self.is_dirty = true;
395        self.offset_x = x * (2.0 / self.width as f32);
396        self.offset_y = y * (2.0 / self.height as f32);
397    }
398
399    fn set_scale(&mut self, scale: f32, center_x: i32, center_y: i32) {
400        self.is_dirty = true;
401        self.scale = scale;
402        self.scale_center = (center_x, center_y);
403    }
404
405    fn get_scale(&self) -> (f32, i32, i32) {
406        (self.scale, self.scale_center.0, self.scale_center.1)
407    }
408
409    fn as_any(&self) -> &dyn Any {
410        self
411    }
412
413    fn as_any_mut(&mut self) -> &mut dyn Any {
414        self
415    }
416
417    /// Permits the creation of an arbitrary clipping rectangle. It's a really good idea
418    /// to make sure that this rectangle is entirely valid.
419    fn set_clipping(&mut self, clipping: Option<Rect>) {
420        self.extra_clipping = clipping;
421    }
422
423    /// Returns the current arbitrary clipping rectangle, None if there isn't one.
424    fn get_clipping(&self) -> Option<Rect> {
425        self.extra_clipping
426    }
427
428    /// Sets ALL tiles foreground alpha (only tiles that exist, in sparse consoles).
429    fn set_all_fg_alpha(&mut self, alpha: f32) {
430        self.tiles.iter_mut().for_each(|t| t.fg.a = alpha);
431    }
432
433    /// Sets ALL tiles background alpha (only tiles that exist, in sparse consoles).
434    fn set_all_bg_alpha(&mut self, alpha: f32) {
435        self.tiles.iter_mut().for_each(|t| t.bg.a = alpha);
436    }
437
438    /// Sets ALL tiles foreground alpha (only tiles that exist, in sparse consoles).
439    fn set_all_alpha(&mut self, fg: f32, bg: f32) {
440        self.tiles.iter_mut().for_each(|t| {
441            t.fg.a = fg;
442            t.bg.a = bg;
443        });
444    }
445
446    /// Sets the character translation mode
447    fn set_translation_mode(&mut self, mode: CharacterTranslationMode) {
448        self.translation = mode;
449    }
450
451    /// Sets the character size of the terminal
452    fn set_char_size(&mut self, width: u32, height: u32) {
453        self.width = width;
454        self.height = height;
455        self.needs_resize_internal = true;
456    }
457
458    // Clears the dirty bit
459    fn clear_dirty(&mut self) {
460        self.is_dirty = false;
461    }
462}