bracket_terminal/consoles/text/
textblock.rs

1use crate::prelude::{string_to_cp437, Console, DrawBatch, FontCharType, Tile};
2use bracket_color::prelude::{ColorPair, RGB, RGBA};
3use bracket_geometry::prelude::{Point, Rect};
4use std::cmp;
5
6pub struct TextBlock {
7    x: i32,
8    y: i32,
9    width: i32,
10    height: i32,
11    fg: RGBA,
12    bg: RGBA,
13    buffer: Vec<Tile>,
14    cursor: (i32, i32),
15}
16
17#[derive(Debug, Clone)]
18pub struct OutOfSpace;
19
20impl std::fmt::Display for OutOfSpace {
21    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
22        write!(f, "Out of text-buffer space.")
23    }
24}
25
26impl TextBlock {
27    pub fn new(x: i32, y: i32, width: i32, height: i32) -> TextBlock {
28        TextBlock {
29            x,
30            y,
31            width,
32            height,
33            fg: RGBA::from_f32(1.0, 1.0, 1.0, 1.0),
34            bg: RGBA::from_f32(0.0, 0.0, 0.0, 1.0),
35            buffer: vec![
36                Tile {
37                    glyph: 0,
38                    fg: RGBA::from_f32(1.0, 1.0, 1.0, 1.0),
39                    bg: RGBA::from_f32(0.0, 0.0, 0.0, 1.0)
40                };
41                width as usize * height as usize
42            ],
43            cursor: (0, 0),
44        }
45    }
46
47    pub fn fg<COLOR>(&mut self, fg: RGB)
48    where
49        COLOR: Into<RGBA>,
50    {
51        self.fg = fg.into();
52    }
53
54    pub fn bg<COLOR>(&mut self, bg: COLOR)
55    where
56        COLOR: Into<RGBA>,
57    {
58        self.bg = bg.into();
59    }
60
61    pub fn move_to(&mut self, x: i32, y: i32) {
62        self.cursor = (x, y);
63    }
64
65    pub fn get_cursor(&self) -> Point {
66        Point::from_tuple(self.cursor)
67    }
68
69    pub fn get_origin(&self) -> Point {
70        Point::new(self.x, self.y)
71    }
72
73    pub fn set_origin(&mut self, origin: Point) {
74        self.x = origin.x;
75        self.y = origin.y;
76    }
77
78    fn at(&self, x: i32, y: i32) -> usize {
79        ((y * self.width) + x) as usize
80    }
81
82    pub fn render(&self, mut console: impl AsMut<dyn Console>) {
83        for y in 0..self.height {
84            for x in 0..self.width {
85                console.as_mut().set(
86                    x + self.x,
87                    y + self.y,
88                    self.buffer[self.at(x, y)].fg,
89                    self.buffer[self.at(x, y)].bg,
90                    self.buffer[self.at(x, y)].glyph,
91                );
92            }
93        }
94    }
95
96    pub fn render_to_draw_batch(&self, draw_batch: &mut DrawBatch) {
97        for y in 0..self.height {
98            for x in 0..self.width {
99                draw_batch.set(
100                    Point::new(x + self.x, y + self.y),
101                    ColorPair::new(self.buffer[self.at(x, y)].fg, self.buffer[self.at(x, y)].bg),
102                    self.buffer[self.at(x, y)].glyph,
103                );
104            }
105        }
106    }
107
108    pub fn render_to_draw_batch_clip(&self, draw_batch: &mut DrawBatch, clip: &Rect) {
109        for y in cmp::max(0, clip.y1)..cmp::min(self.height, clip.y2) {
110            for x in cmp::max(0, clip.x1)..cmp::min(self.width, clip.x2) {
111                draw_batch.set(
112                    Point::new(x + self.x, y + self.y),
113                    ColorPair::new(self.buffer[self.at(x, y)].fg, self.buffer[self.at(x, y)].bg),
114                    self.buffer[self.at(x, y)].glyph,
115                );
116            }
117        }
118    }
119
120    pub fn print(&mut self, text: &TextBuilder) -> Result<(), OutOfSpace> {
121        for cmd in &text.commands {
122            match cmd {
123                CommandType::Text { block: t } => {
124                    for c in t {
125                        let idx = self.at(self.cursor.0, self.cursor.1);
126                        if idx < self.buffer.len() {
127                            self.buffer[idx].glyph = *c;
128                            self.buffer[idx].fg = self.fg;
129                            self.buffer[idx].bg = self.bg;
130                            self.cursor.0 += 1;
131                            if self.cursor.0 >= self.width {
132                                self.cursor.0 = 0;
133                                self.cursor.1 += 1;
134                            }
135                        } else {
136                            return Err(OutOfSpace);
137                        }
138                    }
139                }
140
141                CommandType::Centered { block: t } => {
142                    let text_width = t.len() as i32;
143                    let half_width = text_width / 2;
144                    self.cursor.0 = (self.width / 2) - half_width;
145                    for c in t {
146                        let idx = self.at(self.cursor.0, self.cursor.1);
147                        if idx < self.buffer.len() {
148                            self.buffer[idx].glyph = *c;
149                            self.buffer[idx].fg = self.fg;
150                            self.buffer[idx].bg = self.bg;
151                            self.cursor.0 += 1;
152                            if self.cursor.0 >= self.width {
153                                self.cursor.0 = 0;
154                                self.cursor.1 += 1;
155                            }
156                        } else {
157                            return Err(OutOfSpace);
158                        }
159                    }
160                }
161
162                CommandType::NewLine {} => {
163                    self.cursor.0 = 0;
164                    self.cursor.1 += 1;
165                }
166
167                CommandType::Foreground { col } => self.fg = *col,
168                CommandType::Background { col } => self.bg = *col,
169                CommandType::Reset {} => {
170                    self.cursor = (0, 0);
171                    self.fg = RGBA::from_f32(1.0, 1.0, 1.0, 1.0);
172                    self.bg = RGBA::from_f32(0.0, 0.0, 0.0, 1.0);
173                }
174
175                CommandType::TextWrapper { block: t } => {
176                    for word in t.split(' ') {
177                        let mut chrs = string_to_cp437(&word);
178                        chrs.push(32);
179                        if self.cursor.0 + chrs.len() as i32 >= self.width {
180                            self.cursor.0 = 0;
181                            self.cursor.1 += 1;
182                        }
183                        for c in chrs {
184                            let idx = self.at(self.cursor.0, self.cursor.1);
185                            if idx < self.buffer.len() {
186                                self.buffer[idx].glyph = c;
187                                self.buffer[idx].fg = self.fg;
188                                self.buffer[idx].bg = self.bg;
189                                self.cursor.0 += 1;
190                                if self.cursor.0 >= self.width {
191                                    self.cursor.0 = 0;
192                                    self.cursor.1 += 1;
193                                }
194                            } else {
195                                return Err(OutOfSpace);
196                            }
197                        }
198                    }
199                }
200            }
201        }
202        Ok(())
203    }
204}
205
206pub enum CommandType {
207    Text { block: Vec<FontCharType> },
208    Centered { block: Vec<FontCharType> },
209    NewLine {},
210    Foreground { col: RGBA },
211    Background { col: RGBA },
212    TextWrapper { block: String },
213    Reset {},
214}
215
216pub struct TextBuilder {
217    commands: Vec<CommandType>,
218}
219
220impl TextBuilder {
221    pub fn empty() -> TextBuilder {
222        TextBuilder {
223            commands: Vec::new(),
224        }
225    }
226
227    pub fn append(&mut self, text: &str) -> &mut Self {
228        let chrs = string_to_cp437(&text);
229        self.commands.push(CommandType::Text { block: chrs });
230        self
231    }
232    pub fn centered(&mut self, text: &str) -> &mut Self {
233        let chrs = string_to_cp437(&text);
234        self.commands.push(CommandType::Centered { block: chrs });
235        self
236    }
237    pub fn reset(&mut self) -> &mut Self {
238        self.commands.push(CommandType::Reset {});
239        self
240    }
241    pub fn ln(&mut self) -> &mut Self {
242        self.commands.push(CommandType::NewLine {});
243        self
244    }
245    pub fn fg<COLOR>(&mut self, col: COLOR) -> &mut Self
246    where
247        COLOR: Into<RGBA>,
248    {
249        self.commands
250            .push(CommandType::Foreground { col: col.into() });
251        self
252    }
253    pub fn bg<COLOR>(&mut self, col: COLOR) -> &mut Self
254    where
255        COLOR: Into<RGBA>,
256    {
257        self.commands
258            .push(CommandType::Background { col: col.into() });
259        self
260    }
261    pub fn line_wrap(&mut self, text: &str) -> &mut Self {
262        self.commands.push(CommandType::TextWrapper {
263            block: text.to_string(),
264        });
265        self
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::{TextBlock, TextBuilder};
272
273    #[test]
274    fn textblock_ok() {
275        let mut block = TextBlock::new(0, 0, 80, 25);
276
277        let mut buf = TextBuilder::empty();
278        buf.ln()
279            .centered("Hello World")
280            .line_wrap("The quick brown fox jumped over the lazy dog, and just kept on running in an attempt to exceed the console width.")
281            .reset();
282
283        assert!(block.print(&buf).is_ok());
284    }
285
286    #[test]
287    fn textblock_wrap_error() {
288        let mut block = TextBlock::new(0, 0, 80, 2);
289
290        let mut buf = TextBuilder::empty();
291        buf.ln()
292            .centered("Hello World")
293            .line_wrap("The quick brown fox jumped over the lazy dog, and just kept on running in an attempt to exceed the console width.")
294            .line_wrap("The quick brown fox jumped over the lazy dog, and just kept on running in an attempt to exceed the console width.")
295            .reset();
296
297        assert!(block.print(&buf).is_err());
298    }
299}