bracket_terminal/consoles/text/
textblock.rs1use 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}