Skip to main content

par_term_render/cell_renderer/
atlas.rs

1use super::{CellRenderer, GlyphInfo};
2
3pub(crate) struct RasterizedGlyph {
4    pub width: u32,
5    pub height: u32,
6    #[allow(dead_code)]
7    pub bearing_x: f32,
8    #[allow(dead_code)]
9    pub bearing_y: f32,
10    pub pixels: Vec<u8>,
11    pub is_colored: bool,
12}
13
14impl CellRenderer {
15    pub fn clear_glyph_cache(&mut self) {
16        self.glyph_cache.clear();
17        self.lru_head = None;
18        self.lru_tail = None;
19        self.atlas_next_x = 0;
20        self.atlas_next_y = 0;
21        self.atlas_row_height = 0;
22        self.dirty_rows.fill(true);
23        // Re-upload the solid white pixel for geometric block rendering
24        self.upload_solid_pixel();
25    }
26
27    pub(crate) fn lru_remove(&mut self, key: u64) {
28        let info = self.glyph_cache.get(&key).unwrap();
29        let prev = info.prev;
30        let next = info.next;
31
32        if let Some(p) = prev {
33            self.glyph_cache.get_mut(&p).unwrap().next = next;
34        } else {
35            self.lru_head = next;
36        }
37
38        if let Some(n) = next {
39            self.glyph_cache.get_mut(&n).unwrap().prev = prev;
40        } else {
41            self.lru_tail = prev;
42        }
43    }
44
45    pub(crate) fn lru_push_front(&mut self, key: u64) {
46        let next = self.lru_head;
47        if let Some(n) = next {
48            self.glyph_cache.get_mut(&n).unwrap().prev = Some(key);
49        } else {
50            self.lru_tail = Some(key);
51        }
52
53        let info = self.glyph_cache.get_mut(&key).unwrap();
54        info.prev = None;
55        info.next = next;
56        self.lru_head = Some(key);
57    }
58
59    pub(crate) fn rasterize_glyph(
60        &self,
61        font_idx: usize,
62        glyph_id: u16,
63    ) -> Option<RasterizedGlyph> {
64        let font = self.font_manager.get_font(font_idx)?;
65        // Use swash to rasterize
66        use swash::scale::image::Content;
67        use swash::scale::{Render, ScaleContext};
68        use swash::zeno::Format;
69
70        let mut context = ScaleContext::new();
71
72        // Apply hinting based on config setting
73        let mut scaler = context
74            .builder(*font)
75            .size(self.font_size_pixels)
76            .hint(self.font_hinting)
77            .build();
78
79        // Determine render format based on anti-aliasing and thin strokes settings
80        let use_thin_strokes = self.should_use_thin_strokes();
81        let render_format = if !self.font_antialias {
82            // No anti-aliasing: render as alpha mask (will be thresholded)
83            Format::Alpha
84        } else if use_thin_strokes {
85            // Thin strokes: use subpixel rendering for lighter appearance
86            Format::Subpixel
87        } else {
88            // Standard anti-aliased rendering
89            Format::Alpha
90        };
91
92        // Try color sources first so emoji fonts (Apple Color Emoji,
93        // Noto Color Emoji) render as colored bitmaps. Regular text fonts
94        // have no color data so will fall through to Outline automatically.
95        let image = Render::new(&[
96            swash::scale::Source::ColorBitmap(swash::scale::StrikeWith::BestFit),
97            swash::scale::Source::ColorOutline(0),
98            swash::scale::Source::Outline,
99        ])
100        .format(render_format)
101        .render(&mut scaler, glyph_id)?;
102
103        let (pixels, is_colored) = match image.content {
104            Content::Color => (image.data.clone(), true),
105            Content::Mask => {
106                let mut pixels = Vec::with_capacity(image.data.len() * 4);
107                for &mask in &image.data {
108                    // If anti-aliasing is disabled, threshold the alpha to create crisp edges
109                    let alpha = if !self.font_antialias {
110                        if mask > 127 { 255 } else { 0 }
111                    } else {
112                        mask
113                    };
114                    pixels.extend_from_slice(&[255, 255, 255, alpha]);
115                }
116                (pixels, false)
117            }
118            Content::SubpixelMask => {
119                let pixels = convert_subpixel_mask_to_rgba(&image);
120                (pixels, false)
121            }
122        };
123
124        Some(RasterizedGlyph {
125            width: image.placement.width,
126            height: image.placement.height,
127            bearing_x: image.placement.left as f32,
128            bearing_y: image.placement.top as f32,
129            pixels,
130            is_colored,
131        })
132    }
133
134    pub(crate) fn upload_glyph(&mut self, _key: u64, raster: &RasterizedGlyph) -> GlyphInfo {
135        let padding = 2;
136        if self.atlas_next_x + raster.width + padding > 2048 {
137            self.atlas_next_x = 0;
138            self.atlas_next_y += self.atlas_row_height + padding;
139            self.atlas_row_height = 0;
140        }
141
142        if self.atlas_next_y + raster.height + padding > 2048 {
143            self.clear_glyph_cache();
144        }
145
146        let info = GlyphInfo {
147            key: _key,
148            x: self.atlas_next_x,
149            y: self.atlas_next_y,
150            width: raster.width,
151            height: raster.height,
152            bearing_x: raster.bearing_x,
153            bearing_y: raster.bearing_y,
154            is_colored: raster.is_colored,
155            prev: None,
156            next: None,
157        };
158
159        self.queue.write_texture(
160            wgpu::TexelCopyTextureInfo {
161                texture: &self.atlas_texture,
162                mip_level: 0,
163                origin: wgpu::Origin3d {
164                    x: info.x,
165                    y: info.y,
166                    z: 0,
167                },
168                aspect: wgpu::TextureAspect::All,
169            },
170            &raster.pixels,
171            wgpu::TexelCopyBufferLayout {
172                offset: 0,
173                bytes_per_row: Some(4 * raster.width),
174                rows_per_image: Some(raster.height),
175            },
176            wgpu::Extent3d {
177                width: raster.width,
178                height: raster.height,
179                depth_or_array_layers: 1,
180            },
181        );
182
183        self.atlas_next_x += raster.width + padding;
184        self.atlas_row_height = self.atlas_row_height.max(raster.height);
185
186        info
187    }
188}
189
190/// Convert a swash subpixel mask into an RGBA alpha mask.
191/// Some swash builds emit 3 bytes/pixel (RGB), others 4 bytes/pixel (RGBA).
192/// We derive alpha from luminance of RGB and ignore the packed alpha to avoid
193/// dropping coverage when alpha is zeroed by the rasterizer.
194fn convert_subpixel_mask_to_rgba(image: &swash::scale::image::Image) -> Vec<u8> {
195    let width = image.placement.width as usize;
196    let height = image.placement.height as usize;
197    let mut pixels = Vec::with_capacity(width * height * 4);
198
199    let stride = if width > 0 && height > 0 {
200        image.data.len() / (width * height)
201    } else {
202        0
203    };
204
205    match stride {
206        3 => {
207            for chunk in image.data.chunks_exact(3) {
208                let r = chunk[0];
209                let g = chunk[1];
210                let b = chunk[2];
211                let alpha = ((r as u32 * 299 + g as u32 * 587 + b as u32 * 114) / 1000) as u8;
212                pixels.extend_from_slice(&[255, 255, 255, alpha]);
213            }
214        }
215        4 => {
216            for chunk in image.data.chunks_exact(4) {
217                let r = chunk[0];
218                let g = chunk[1];
219                let b = chunk[2];
220                // Ignore chunk[3] because it can be zeroed in some builds.
221                let alpha = ((r as u32 * 299 + g as u32 * 587 + b as u32 * 114) / 1000) as u8;
222                pixels.extend_from_slice(&[255, 255, 255, alpha]);
223            }
224        }
225        _ => {
226            // Fallback: treat as opaque white to avoid invisibility if layout changes.
227            pixels.resize(width * height * 4, 255);
228        }
229    }
230
231    pixels
232}
233
234#[cfg(test)]
235mod tests {
236    use super::convert_subpixel_mask_to_rgba;
237    use swash::scale::{Render, ScaleContext, Source};
238    use swash::zeno::Format;
239
240    #[test]
241    fn subpixel_mask_uses_rgba_stride() {
242        let data = std::fs::read("../par-term-fonts/fonts/DejaVuSansMono.ttf").expect("font file");
243        let font = swash::FontRef::from_index(&data, 0).expect("font ref");
244        let mut context = ScaleContext::new();
245        let glyph_id = font.charmap().map('a');
246        let mut scaler = context.builder(font).size(18.0).hint(true).build();
247
248        let image = Render::new(&[
249            Source::ColorOutline(0),
250            Source::ColorBitmap(swash::scale::StrikeWith::BestFit),
251            Source::Outline,
252            Source::Bitmap(swash::scale::StrikeWith::BestFit),
253        ])
254        .format(Format::Subpixel)
255        .render(&mut scaler, glyph_id)
256        .expect("render");
257
258        let converted = convert_subpixel_mask_to_rgba(&image);
259
260        let width = image.placement.width as usize;
261        let height = image.placement.height as usize;
262        let mut expected = Vec::with_capacity(width * height * 4);
263        let stride = if width > 0 && height > 0 {
264            image.data.len() / (width * height)
265        } else {
266            0
267        };
268
269        match stride {
270            3 => {
271                for chunk in image.data.chunks_exact(3) {
272                    let r = chunk[0];
273                    let g = chunk[1];
274                    let b = chunk[2];
275                    let alpha = ((r as u32 * 299 + g as u32 * 587 + b as u32 * 114) / 1000) as u8;
276                    expected.extend_from_slice(&[255, 255, 255, alpha]);
277                }
278            }
279            4 => {
280                for chunk in image.data.chunks_exact(4) {
281                    let r = chunk[0];
282                    let g = chunk[1];
283                    let b = chunk[2];
284                    let alpha = ((r as u32 * 299 + g as u32 * 587 + b as u32 * 114) / 1000) as u8;
285                    expected.extend_from_slice(&[255, 255, 255, alpha]);
286                }
287            }
288            _ => expected.resize(width * height * 4, 255),
289        }
290
291        assert_eq!(converted, expected);
292    }
293}