ltrender/drawable_traits/
basic_draw_creator.rs1use std::collections::HashMap;
2
3use ascii_assets::{Color, TerminalChar};
4use common_stdx::{Point, Rect};
5
6use crate::BasicDraw;
7
8#[derive(Debug, Clone)]
9pub struct VerticalBarGraphConfig {
10 pub height: i32,
12
13 pub bar_width: i32,
15
16 pub spacing: i32,
18
19 pub fill_char: char,
21
22 pub color: Color,
24}
25
26impl Default for VerticalBarGraphConfig {
27 fn default() -> Self {
28 Self {
29 height: 10,
30 bar_width: 1,
31 spacing: 1,
32 fill_char: '█',
33 color: Color::White,
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
39pub struct LineGraphConfig {
40 pub height: i32,
42
43 pub spacing: i32,
45
46 pub point_char: char,
48
49 pub line_char: char,
51
52 pub point_color: Color,
54
55 pub line_color: Color,
57}
58
59impl Default for LineGraphConfig {
60 fn default() -> Self {
61 Self {
62 height: 10,
63 spacing: 3,
64 point_char: '•',
65 line_char: '─',
66 point_color: Color::White,
67 line_color: Color::White,
68 }
69 }
70}
71
72#[derive(Debug, Default)]
74pub struct BasicDrawCreator {
75 pub draws: HashMap<Point<i32>, TerminalChar>,
76}
77
78impl BasicDrawCreator {
79 pub fn new() -> Self {
80 Self::default()
81 }
82 pub fn new_with_capacity(cap: usize) -> Self {
83 Self {
84 draws: HashMap::with_capacity(cap),
85 }
86 }
87
88 pub fn draw_char(&mut self, pos: impl Into<Point<i32>>, chr: TerminalChar) {
92 self.draws.insert(pos.into(), chr);
93 }
94
95 pub fn draw_line(
99 &mut self,
100 p1: impl Into<Point<i32>>,
101 p2: impl Into<Point<i32>>,
102 chr: TerminalChar,
103 ) {
104 let p1 = p1.into();
105 let p2 = p2.into();
106 let mut x0 = p1.x;
107 let mut y0 = p1.y;
108 let x1 = p2.x;
109 let y1 = p2.y;
110
111 let dx = (x1 - x0).abs();
112 let dy = (y1 - y0).abs();
113 let sx = if x0 < x1 { 1 } else { -1 };
114 let sy = if y0 < y1 { 1 } else { -1 };
115 let mut err = dx - dy;
116
117 loop {
118 self.draws.insert(Point { x: x0, y: y0 }, chr);
119
120 if x0 == x1 && y0 == y1 {
121 break;
122 }
123
124 let e2 = 2 * err;
125 if e2 > -dy {
126 err -= dy;
127 x0 += sx;
128 }
129 if e2 < dx {
130 err += dx;
131 y0 += sy;
132 }
133 }
134 }
135
136 pub fn draw_text(&mut self, pos: impl Into<Point<i32>>, text: &str, fg: Option<Color>) {
140 let start = pos.into();
141 let mut x = start.x;
142 let mut y = start.y;
143
144 for ch in text.chars() {
145 match ch {
146 '\n' => {
147 y += 1;
148 x = start.x;
149 }
150 _ => {
151 let mut t = TerminalChar::from_char(ch);
152 if let Some(col) = fg {
153 t = t.set_fg(col);
154 }
155 self.draws.insert(Point::new(x, y), t);
156 x += 1;
157 }
158 }
159 }
160 }
161
162 pub fn string_to_basic_draws(
164 pos: impl Into<Point<i32>>,
165 text: &str,
166 fg: Option<Color>,
167 ) -> Vec<BasicDraw> {
168 let mut tmp = BasicDrawCreator::new_with_capacity(text.len());
169 tmp.draw_text(pos.into(), text, fg);
170 tmp.dump_draws()
171 }
172
173 pub fn draw_vertical_bar_graph_simple(
174 &mut self,
175 start: Point<i32>,
176 values: &[usize],
177 height: i32,
178 color: Color,
179 ) -> i32 {
180 let cfg = VerticalBarGraphConfig {
181 height,
182 color,
183 ..Default::default()
184 };
185
186 self.draw_vertical_bar_graph(start, values, &cfg)
187 }
188
189 pub fn draw_vertical_bar_graph(
191 &mut self,
192 start: Point<i32>,
193 values: &[usize],
194 cfg: &VerticalBarGraphConfig,
195 ) -> i32 {
196 if values.is_empty() || cfg.height <= 0 || cfg.bar_width <= 0 {
197 return start.y;
198 }
199
200 let mut max_val = 1usize;
201 for &v in values {
202 max_val = max_val.max(v);
203 }
204
205 let fill = TerminalChar::from_char(cfg.fill_char).set_fg(cfg.color);
206
207 for (i, &v) in values.iter().enumerate() {
208 let x = start.x + (i as i32) * (cfg.bar_width + cfg.spacing);
209 let bar_h = ((v as f32 / max_val as f32) * (cfg.height as f32)).round() as i32;
210
211 for bx in 0..cfg.bar_width {
212 for dy in 0..bar_h {
213 let pos = Point::new(x + bx, start.y + cfg.height - 1 - dy);
214 self.draws.insert(pos, fill);
215 }
216 }
217 }
218
219 start.y + cfg.height
220 }
221
222 pub fn draw_line_graph(
225 &mut self,
226 start: Point<i32>,
227 values: &[usize],
228 cfg: &LineGraphConfig,
229 ) -> i32 {
230 if values.is_empty() || cfg.height <= 0 || cfg.spacing <= 0 {
231 return start.y;
232 }
233
234 let mut max_val = 1usize;
235 for &v in values {
236 max_val = max_val.max(v);
237 }
238
239 let point_t = TerminalChar::from_char(cfg.point_char).set_fg(cfg.point_color);
240 let line_t = TerminalChar::from_char(cfg.line_char).set_fg(cfg.line_color);
241
242 let mut prev_point: Option<Point<i32>> = None;
243
244 for (i, &v) in values.iter().enumerate() {
245 let x = start.x + (i as i32) * cfg.spacing;
246 let y_off = ((v as f32 / max_val as f32) * (cfg.height as f32)).round() as i32;
247 let y = start.y + cfg.height - 1 - y_off;
248 let pt = Point::new(x, y);
249
250 self.draws.insert(pt, point_t);
251
252 if let Some(prev) = prev_point {
253 self.draw_line(prev, pt, line_t);
254 }
255
256 prev_point = Some(pt);
257 }
258
259 start.y + cfg.height
260 }
261 pub fn draw_cell(
263 &mut self,
264 pos: impl Into<Point<i32>>,
265 ch: char,
266 fg: Color,
267 bg: Option<Color>,
268 ) {
269 let mut t = TerminalChar::from_char(ch).set_fg(fg);
270 if let Some(bgcol) = bg {
271 t = t.set_bg(bgcol);
272 }
273 self.draws.insert(pos.into(), t);
274 }
275
276 pub fn draw_grid_from_fn<F>(
278 &mut self,
279 origin: Point<i32>,
280 cols: usize,
281 rows: usize,
282 stride_x: i32,
283 stride_y: i32,
284 mut cell_fn: F,
285 ) where
286 F: FnMut(usize, usize) -> (char, Color, Option<Color>),
287 {
288 for row in 0..rows {
289 let y = origin.y + (row as i32) * stride_y;
290 for col in 0..cols {
291 let x = origin.x + (col as i32) * stride_x;
292 let (ch, fg, bg) = cell_fn(col, row);
293 self.draw_cell(Point::new(x, y), ch, fg, bg);
294
295 if stride_x >= 2 {
296 let mut t = TerminalChar::from_char(' ');
297 if let Some(bgcol) = bg {
298 t = t.set_bg(bgcol);
299 }
300 self.draws.insert(Point::new(x + 1, y), t);
301 }
302 }
303 }
304 }
305
306 pub fn draw_horizontal_bar(
308 &mut self,
309 pos: impl Into<Point<i32>>,
310 len: i32,
311 chr: char,
312 fg: Color,
313 ) {
314 let p = pos.into();
315 let t = TerminalChar::from_char(chr).set_fg(fg);
316 for dx in 0..len {
317 self.draws.insert(Point::new(p.x + dx, p.y), t);
318 }
319 }
320
321 pub fn get_bounding_box(&self) -> Rect<i32> {
325 if self.draws.is_empty() {
326 return Rect::from_coords(0, 0, 0, 0);
327 }
328
329 let mut min_x = i32::MAX;
330 let mut min_y = i32::MAX;
331 let mut max_x = i32::MIN;
332 let mut max_y = i32::MIN;
333
334 for p in self.draws.keys() {
335 min_x = min_x.min(p.x);
336 min_y = min_y.min(p.y);
337 max_x = max_x.max(p.x);
338 max_y = max_y.max(p.y);
339 }
340
341 Rect::from_coords(min_x, min_y, max_x, max_y)
342 }
343
344 pub fn merge_creator(&mut self, to_merge: BasicDrawCreator) {
348 self.draws.extend(to_merge.draws);
349 }
350
351 pub fn merge_creator_offset(&mut self, to_merge: BasicDrawCreator, offset: Point<i32>) {
353 for (pos, chr) in to_merge.draws {
354 self.draws.insert(pos + offset, chr);
355 }
356 }
357
358 pub fn align_to_origin(&mut self, origin: Point<i32>) {
360 let tl = self.get_bounding_box().p1;
361 let offset = origin - tl;
362
363 let mut new_draws = HashMap::with_capacity(self.draws.len());
364 for (pos, chr) in std::mem::take(&mut self.draws) {
365 new_draws.insert(pos + offset, chr);
366 }
367
368 self.draws = new_draws;
369 }
370
371 pub fn dump_draws(&mut self) -> Vec<BasicDraw> {
373 std::mem::take(&mut self.draws)
374 .into_iter()
375 .map(|(pos, chr)| BasicDraw { pos, chr })
376 .collect()
377 }
378}