Skip to main content

forge_ui/
paint.rs

1use crate::Color;
2use fontdue::{Font, FontSettings, Metrics};
3use serde::Serialize;
4use std::{collections::HashMap, io::Write, path::Path};
5
6#[derive(Clone, Copy, Debug, Default, Serialize, PartialEq)]
7pub struct Rect {
8    pub x: f32,
9    pub y: f32,
10    pub w: f32,
11    pub h: f32,
12}
13impl Rect {
14    pub const fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
15        Self { x, y, w, h }
16    }
17    pub fn contains(self, x: f32, y: f32) -> bool {
18        x >= self.x && y >= self.y && x < self.x + self.w && y < self.y + self.h
19    }
20    pub fn inset(self, n: f32) -> Self {
21        Self::new(
22            self.x + n,
23            self.y + n,
24            (self.w - 2.0 * n).max(0.0),
25            (self.h - 2.0 * n).max(0.0),
26        )
27    }
28    pub fn intersect(self, other: Self) -> Self {
29        let x = self.x.max(other.x);
30        let y = self.y.max(other.y);
31        Self::new(
32            x,
33            y,
34            (self.x + self.w).min(other.x + other.w).max(x) - x,
35            (self.y + self.h).min(other.y + other.h).max(y) - y,
36        )
37    }
38}
39/// CPU rasterizer. All public drawing coordinates are logical pixels.
40/// Glyphs are cached by character, weight and physical pixel size.
41pub struct Painter {
42    pub pixels: Vec<u32>,
43    pub width: u32,
44    pub height: u32,
45    pub scale: f32,
46    fonts: [Font; 2],
47    glyphs: HashMap<(char, u32, bool), (Metrics, Vec<u8>)>,
48    clip: Rect,
49}
50impl Default for Painter {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55impl Painter {
56    pub fn new() -> Self {
57        Self {
58            pixels: vec![],
59            width: 0,
60            height: 0,
61            scale: 1.0,
62            fonts: [
63                Font::from_bytes(
64                    include_bytes!("../assets/DejaVuSans.ttf") as &[u8],
65                    FontSettings::default(),
66                )
67                .unwrap(),
68                Font::from_bytes(
69                    include_bytes!("../assets/DejaVuSans-Bold.ttf") as &[u8],
70                    FontSettings::default(),
71                )
72                .unwrap(),
73            ],
74            glyphs: HashMap::new(),
75            clip: Rect::default(),
76        }
77    }
78    pub fn resize(&mut self, width: u32, height: u32, scale: f32) {
79        self.width = width;
80        self.height = height;
81        self.scale = scale.max(0.25);
82        self.pixels.resize(width as usize * height as usize, 0);
83        self.clip = Rect::new(
84            0.0,
85            0.0,
86            width as f32 / self.scale,
87            height as f32 / self.scale,
88        );
89    }
90    pub fn clear(&mut self, c: Color) {
91        self.pixels.fill(c.0);
92    }
93    pub fn set_clip(&mut self, rect: Rect) {
94        self.clip = rect.intersect(Rect::new(
95            0.0,
96            0.0,
97            self.width as f32 / self.scale,
98            self.height as f32 / self.scale,
99        ));
100    }
101    pub fn clip(&self) -> Rect {
102        self.clip
103    }
104    pub fn measure(&self, text: &str, size: f32, bold: bool) -> f32 {
105        let f = &self.fonts[bold as usize];
106        let mut prev = None;
107        let mut x = 0.0;
108        for ch in text.chars() {
109            if let Some(p) = prev {
110                x += f.horizontal_kern(p, ch, size).unwrap_or(0.0);
111            }
112            x += f.metrics(ch, size).advance_width;
113            prev = Some(ch);
114        }
115        x
116    }
117    fn blend(&mut self, x: i32, y: i32, c: Color, alpha: u8) {
118        if x < 0
119            || y < 0
120            || x >= self.width as i32
121            || y >= self.height as i32
122            || !self
123                .clip
124                .contains((x as f32 + 0.5) / self.scale, (y as f32 + 0.5) / self.scale)
125        {
126            return;
127        }
128        let i = y as usize * self.width as usize + x as usize;
129        self.pixels[i] = Color(self.pixels[i]).mix(c, alpha as f32 / 255.0).0;
130    }
131    pub fn rect(&mut self, r: Rect, c: Color, radius: f32) {
132        let clip = r.intersect(self.clip);
133        let s = self.scale;
134        let rad = radius.max(0.0).min(r.w / 2.0).min(r.h / 2.0);
135        for y in (clip.y * s).floor().max(0.0) as i32
136            ..((clip.y + clip.h) * s).ceil().min(self.height as f32) as i32
137        {
138            for x in (clip.x * s).floor().max(0.0) as i32
139                ..((clip.x + clip.w) * s).ceil().min(self.width as f32) as i32
140            {
141                let px = (x as f32 + 0.5) / s;
142                let py = (y as f32 + 0.5) / s;
143                let cx = px.clamp(r.x + rad, r.x + r.w - rad);
144                let cy = py.clamp(r.y + rad, r.y + r.h - rad);
145                let d = ((px - cx).powi(2) + (py - cy).powi(2)).sqrt();
146                let a = if rad == 0.0 {
147                    1.0
148                } else {
149                    ((rad - d) * s + 0.5).clamp(0.0, 1.0)
150                };
151                if a > 0.0 {
152                    self.blend(x, y, c, (a * 255.0) as u8);
153                }
154            }
155        }
156    }
157    pub fn outline(&mut self, r: Rect, c: Color) {
158        self.rect(Rect::new(r.x, r.y, r.w, 1.0), c, 0.0);
159        self.rect(Rect::new(r.x, r.y + r.h - 1.0, r.w, 1.0), c, 0.0);
160        self.rect(Rect::new(r.x, r.y, 1.0, r.h), c, 0.0);
161        self.rect(Rect::new(r.x + r.w - 1.0, r.y, 1.0, r.h), c, 0.0);
162    }
163    pub fn text(&mut self, text: &str, x: f32, y: f32, size: f32, c: Color, bold: bool) {
164        let s = self.scale;
165        let size_px = (size * s * 64.0).round() as u32;
166        let mut pen = x * s;
167        let baseline = (y + size) * s;
168        let mut prev = None;
169        for ch in text.chars() {
170            let f = &self.fonts[bold as usize];
171            if let Some(p) = prev {
172                pen += f
173                    .horizontal_kern(p, ch, size_px as f32 / 64.0)
174                    .unwrap_or(0.0);
175            }
176            let key = (ch, size_px, bold);
177            if !self.glyphs.contains_key(&key) {
178                // Bound the cache even when an app cycles through arbitrary font sizes.
179                if self.glyphs.len() > 8192 {
180                    self.glyphs.clear();
181                }
182                self.glyphs.insert(
183                    key,
184                    self.fonts[bold as usize].rasterize(ch, size_px as f32 / 64.0),
185                );
186            }
187            let (m, bitmap) = &self.glyphs[&key];
188            let m = *m;
189            let gx = pen.round() as i32 + m.xmin;
190            let gy = baseline.round() as i32 - m.ymin - m.height as i32;
191            // Mutate pixels directly so the cached bitmap is never cloned per glyph.
192            for yy in 0..m.height {
193                for xx in 0..m.width {
194                    let px = gx + xx as i32;
195                    let py = gy + yy as i32;
196                    if px < 0
197                        || py < 0
198                        || px >= self.width as i32
199                        || py >= self.height as i32
200                        || !self
201                            .clip
202                            .contains((px as f32 + 0.5) / s, (py as f32 + 0.5) / s)
203                    {
204                        continue;
205                    }
206                    let a = bitmap[yy * m.width + xx];
207                    let i = py as usize * self.width as usize + px as usize;
208                    if a > 0 {
209                        self.pixels[i] = Color(self.pixels[i]).mix(c, a as f32 / 255.0).0;
210                    }
211                }
212            }
213            pen += m.advance_width;
214            prev = Some(ch);
215        }
216    }
217    /// Portable lossless screenshot. Refuses to overwrite an existing file.
218    pub fn save_ppm(&self, path: &Path) -> std::io::Result<()> {
219        let file = std::fs::OpenOptions::new()
220            .write(true)
221            .create_new(true)
222            .open(path)?;
223        let mut out = std::io::BufWriter::new(file);
224        write!(out, "P6\n{} {}\n255\n", self.width, self.height)?;
225        for p in &self.pixels {
226            out.write_all(&[(p >> 16) as u8, (p >> 8) as u8, *p as u8])?;
227        }
228        out.flush()
229    }
230}