Skip to main content

ltrender/drawable_traits/
basic_draw_creator.rs

1use 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    /// Height of the graph in terminal cells
11    pub height: i32,
12
13    /// Width of each bar in characters
14    pub bar_width: i32,
15
16    /// Horizontal spacing between bars
17    pub spacing: i32,
18
19    /// Character used to fill bars
20    pub fill_char: char,
21
22    /// Bar color
23    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    /// Height of the graph in terminal cells
41    pub height: i32,
42
43    /// Horizontal spacing between sample points
44    pub spacing: i32,
45
46    /// Character used to draw sample points
47    pub point_char: char,
48
49    /// Character used to draw connecting lines
50    pub line_char: char,
51
52    /// Color for sample points
53    pub point_color: Color,
54
55    /// Color for connecting lines
56    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/// Collector for drawable terminal operations.
73#[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    /// Draw a single character at `pos`.
89    ///
90    /// If a character already exists at this position, it is replaced.
91    pub fn draw_char(&mut self, pos: impl Into<Point<i32>>, chr: TerminalChar) {
92        self.draws.insert(pos.into(), chr);
93    }
94
95    /// Draw a straight line from `p1` to `p2` (inclusive) using Bresenham’s algorithm.
96    ///
97    /// Both endpoints are included. Each point on the line receives `chr`.
98    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    /// Draw a horizontal text string starting at `pos`.
137    /// Each character advances x by 1. Newlines are interpreted (advances y and resets x to start.x).
138    /// If `fg` is `Some(Color)` it will be used
139    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    /// Convert a string into a Vec<BasicDraw>
163    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    /// Draw a vertical bar graph of the supplied config
190    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    /// Draw a line graph using a config struct.
223    /// Returns `start.y + height`
224    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    /// Draw a single cell
262    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    /// the cell fn should return char/fg/bg
277    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    /// Draw a horizontal bar starting at `pos` with `len` characters of `chr`.
307    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    /// Compute the axis-aligned bounding box containing all drawn points.
322    ///
323    /// Returns `(0, 0),(0, 0)` if empty.
324    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    /// Merge another creator into this one.
345    ///
346    /// Incoming characters overwrite existing positions.
347    pub fn merge_creator(&mut self, to_merge: BasicDrawCreator) {
348        self.draws.extend(to_merge.draws);
349    }
350
351    /// Merge another creator, offsetting all positions by `offset`.
352    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    /// Translate all draws so the bounding box top-left aligns with `origin`.
359    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    /// Consume the creator and return all draws as a `Vec<BasicDraw>`.
372    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}