ascii_forge/renderer/
render.rs

1use std::{fmt::Display, marker::PhantomData};
2
3use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
4
5use crate::prelude::*;
6
7/// A macro to simplify rendering lots of items at once.
8/// The Buffer can be anything that implements AsMut<Buffer>
9/// This render will return the location of which the last element finished rendering.
10/**
11`Example`
12```rust
13# use ascii_forge::prelude::*;
14# fn main() -> std::io::Result<()> {
15// Create a buffer
16let mut buffer = Buffer::new((32, 32));
17
18// Render This works! and Another Element! To the window's buffer
19render!(
20    buffer,
21        (16, 16) => [ "This works!" ],
22        (0, 0) => [ "Another Element!" ]
23);
24
25# Ok(())
26# }
27```
28*/
29#[macro_export]
30macro_rules! render {
31    ($buffer:expr, $( $loc:expr => [$($render:expr),* $(,)?]),* $(,)?  ) => {{
32        #[allow(unused_mut)]
33        let mut loc;
34        $(
35            loc = Vec2::from($loc);
36            $(loc = $render.render(loc, $buffer.as_mut()));*;
37            let _ = loc;
38        )*
39        loc
40    }};
41}
42
43/// The main trait that allows for rendering an element at a location to the buffer.
44/// Render's return type is the location the render ended at.
45pub trait Render {
46    /// Render the object to the buffer at the given location.
47    fn render(&self, loc: Vec2, buffer: &mut Buffer) -> Vec2;
48
49    /// Returns the resulting size of the element
50    fn size(&self) -> Vec2 {
51        let mut buf = Buffer::new((u16::MAX, u16::MAX));
52        render!(buf, vec2(0, 0) => [ self ]);
53        buf.shrink();
54        buf.size()
55    }
56
57    /// Render's the element into a clipped view, allowing for clipping easily
58    fn render_clipped(&self, loc: Vec2, clip_size: Vec2, buffer: &mut Buffer) -> Vec2 {
59        let mut buff = Buffer::new((100, 100));
60        render!(buff, vec2(0, 0) => [ self ]);
61        buff.shrink();
62
63        buff.render_clipped(loc, clip_size, buffer)
64    }
65}
66
67/* --------------- Implementations --------------- */
68impl Render for char {
69    fn render(&self, mut loc: Vec2, buffer: &mut Buffer) -> Vec2 {
70        buffer.set(loc, *self);
71        loc.x += self.width().unwrap_or(1).saturating_sub(1) as u16;
72        loc
73    }
74
75    fn size(&self) -> Vec2 {
76        vec2(self.width().unwrap_or(1) as u16, 1)
77    }
78
79    fn render_clipped(&self, loc: Vec2, clip_size: Vec2, buffer: &mut Buffer) -> Vec2 {
80        let char_width = self.width().unwrap_or(1) as u16;
81
82        // Only render if there's enough space for the character
83        if clip_size.x >= char_width && clip_size.y >= 1 {
84            buffer.set(loc, *self);
85            vec2(loc.x + char_width, loc.y)
86        } else {
87            loc
88        }
89    }
90}
91
92impl Render for &str {
93    fn render(&self, loc: Vec2, buffer: &mut Buffer) -> Vec2 {
94        render!(buffer, loc => [ StyledContent::new(ContentStyle::default(), self) ])
95    }
96
97    fn size(&self) -> Vec2 {
98        StyledContent::new(ContentStyle::default(), self).size()
99    }
100
101    fn render_clipped(&self, loc: Vec2, clip_size: Vec2, buffer: &mut Buffer) -> Vec2 {
102        StyledContent::new(ContentStyle::default(), self).render_clipped(loc, clip_size, buffer)
103    }
104}
105
106impl<R: Render + 'static> From<R> for Box<dyn Render> {
107    fn from(value: R) -> Self {
108        Box::new(value)
109    }
110}
111
112impl<R: Into<Box<dyn Render>> + Clone> Render for Vec<R> {
113    fn render(&self, mut loc: Vec2, buffer: &mut Buffer) -> Vec2 {
114        let items: Vec<Box<dyn Render>> = self.iter().map(|x| x.clone().into()).collect();
115        for item in items {
116            loc = render!(buffer, loc => [ item ]);
117        }
118        loc
119    }
120
121    fn render_clipped(&self, mut loc: Vec2, clip_size: Vec2, buffer: &mut Buffer) -> Vec2 {
122        let start_loc = loc;
123        let items: Vec<Box<dyn Render>> = self.iter().map(|x| x.clone().into()).collect();
124
125        for item in items {
126            // Calculate remaining clip space
127            let used_x = loc.x.saturating_sub(start_loc.x);
128            let used_y = loc.y.saturating_sub(start_loc.y);
129
130            if used_y >= clip_size.y {
131                break;
132            }
133
134            let remaining_clip = vec2(
135                clip_size.x.saturating_sub(used_x),
136                clip_size.y.saturating_sub(used_y),
137            );
138
139            if remaining_clip.x == 0 || remaining_clip.y == 0 {
140                break;
141            }
142
143            loc = item.render_clipped(loc, remaining_clip, buffer);
144        }
145        loc
146    }
147}
148
149/// A Render type that doesn't get split. It purely renders the one item to the screen.
150/// Useful for multi-character emojis.
151pub struct CharString<D: Display, F: Into<StyledContent<D>> + Clone> {
152    pub text: F,
153    marker: PhantomData<D>,
154}
155
156impl<D: Display, F: Into<StyledContent<D>> + Clone> CharString<D, F> {
157    pub fn new(text: F) -> Self {
158        Self {
159            text,
160            marker: PhantomData {},
161        }
162    }
163}
164
165impl<D: Display, F: Into<StyledContent<D>> + Clone> Render for CharString<D, F> {
166    fn render(&self, loc: Vec2, buffer: &mut Buffer) -> Vec2 {
167        render!(buffer, loc => [ Cell::styled(self.text.clone().into()) ])
168    }
169
170    fn render_clipped(&self, loc: Vec2, clip_size: Vec2, buffer: &mut Buffer) -> Vec2 {
171        let cell = Cell::styled(self.text.clone().into());
172        let cell_width = cell.width();
173
174        // Only render if there's enough space for the entire cell
175        if clip_size.x >= cell_width && clip_size.y >= 1 {
176            buffer.set(loc, cell);
177            vec2(loc.x + cell_width, loc.y)
178        } else {
179            loc
180        }
181    }
182}
183
184impl Render for String {
185    fn render(&self, loc: Vec2, buffer: &mut Buffer) -> Vec2 {
186        render!(buffer, loc => [ self.as_str() ])
187    }
188
189    fn render_clipped(&self, loc: Vec2, clip_size: Vec2, buffer: &mut Buffer) -> Vec2 {
190        self.as_str().render_clipped(loc, clip_size, buffer)
191    }
192}
193
194impl<D: Display> Render for StyledContent<D> {
195    fn render(&self, mut loc: Vec2, buffer: &mut Buffer) -> Vec2 {
196        let base_x = loc.x;
197        for line in format!("{}", self.content()).split('\n') {
198            loc.x = base_x;
199            for chr in line.chars().collect::<Vec<char>>() {
200                buffer.set(loc, StyledContent::new(*self.style(), chr));
201                loc.x += chr.width().unwrap_or(1) as u16;
202            }
203            loc.y += 1;
204        }
205        loc.y -= 1;
206        loc
207    }
208
209    fn size(&self) -> Vec2 {
210        let mut width = 0;
211        let mut height = 0;
212        for line in format!("{}", self.content()).split('\n') {
213            width = line.chars().count().max(width);
214            height += line.width() as u16;
215        }
216        vec2(width as u16, height)
217    }
218
219    fn render_clipped(&self, mut loc: Vec2, clip_size: Vec2, buffer: &mut Buffer) -> Vec2 {
220        let base_x = loc.x;
221        let start_y = loc.y;
222        let mut lines_rendered = 0;
223
224        for line in format!("{}", self.content()).split('\n') {
225            if lines_rendered >= clip_size.y {
226                break;
227            }
228
229            loc.x = base_x;
230            let mut chars_rendered = 0;
231
232            for chr in line.chars().collect::<Vec<char>>() {
233                let chr_width = chr.width().unwrap_or(1) as u16;
234
235                if chars_rendered + chr_width > clip_size.x {
236                    break;
237                }
238
239                buffer.set(loc, StyledContent::new(*self.style(), chr));
240                loc.x += chr_width;
241                chars_rendered += chr_width;
242            }
243
244            loc.y += 1;
245            lines_rendered += 1;
246        }
247
248        vec2(
249            base_x + lines_rendered.min(clip_size.x),
250            start_y + lines_rendered.min(clip_size.y),
251        )
252    }
253}