feather_ui/render/
text.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2025 Fundament Research Institute <https://fundament.institute>
3
4use std::cell::RefCell;
5use std::rc::Rc;
6
7use cosmic_text::{CacheKey, FontSystem};
8use guillotiere::AllocId;
9
10use crate::color::{Premultiplied, sRGB32};
11use crate::graphics::{GlyphCache, GlyphRegion};
12use crate::render::atlas::{Atlas, Size};
13use crate::render::compositor::{CompositorView, DataFlags};
14use crate::{Error, PxRect};
15
16use swash::scale::{Render, ScaleContext, Source, StrikeWith};
17use swash::zeno::{Format, Vector};
18
19pub use swash::scale::image::{Content, Image};
20pub use swash::zeno::{Angle, Command, Placement, Transform};
21
22pub struct Instance {
23    pub text_buffer: Rc<RefCell<cosmic_text::Buffer>>,
24    pub padding: std::cell::Cell<crate::PxPerimeter>,
25}
26
27impl Instance {
28    pub fn get_glyph(key: CacheKey, glyphs: &GlyphCache) -> Option<&GlyphRegion> {
29        glyphs.get(&key)
30    }
31
32    pub fn draw_glyph(
33        font_system: &mut FontSystem,
34        context: &mut ScaleContext,
35        cache_key: CacheKey,
36    ) -> Option<Image> {
37        let font = match font_system.get_font(cache_key.font_id, cache_key.font_weight) {
38            Some(some) => some,
39            None => {
40                debug_assert!(false, "did not find font {:?}", cache_key.font_id);
41                return None;
42            }
43        };
44
45        // Build the scaler
46        let mut scaler = context
47            .builder(font.as_swash())
48            .size(f32::from_bits(cache_key.font_size_bits))
49            .hint(true)
50            .build();
51
52        // Compute the fractional offset-- you'll likely want to quantize this
53        // in a real renderer
54        let offset = Vector::new(cache_key.x_bin.as_float(), cache_key.y_bin.as_float());
55
56        // Select our source order
57        Render::new(&[
58            // Color outline with the first palette
59            Source::ColorOutline(0),
60            // Color bitmap with best fit selection mode
61            Source::ColorBitmap(StrikeWith::BestFit),
62            // Standard scalable outline
63            Source::Outline,
64        ])
65        // Select a subpixel format
66        .format(Format::Alpha)
67        // Apply the fractional offset
68        .offset(offset)
69        // Render the image
70        .render(&mut scaler, cache_key.glyph_id)
71    }
72
73    pub fn write_glyph(
74        key: CacheKey,
75        font_system: &mut FontSystem,
76        glyphs: &mut GlyphCache,
77        device: &wgpu::Device,
78        queue: &wgpu::Queue,
79        atlas: &mut Atlas,
80        cache: &mut ScaleContext,
81    ) -> Result<(), Error> {
82        if glyphs.get(&key).is_some() {
83            // We can't actually return this borrow because of https://github.com/rust-lang/rust/issues/58910
84            return Ok(());
85        }
86
87        let Some(mut image) = Self::draw_glyph(font_system, cache, key) else {
88            return Err(Error::GlyphRenderFailure);
89        };
90
91        let region = if image.data.is_empty() {
92            super::atlas::Region {
93                id: AllocId::deserialize(u32::MAX),
94                uv: guillotiere::euclid::Box2D::zero(),
95                index: 0,
96            }
97        } else {
98            // Find a position in the packer
99            atlas.reserve(
100                device,
101                Size::new(image.placement.width as i32, image.placement.height as i32),
102                None,
103                None,
104            )?
105        };
106
107        if !image.data.is_empty() {
108            match image.content {
109                Content::Mask => {
110                    let mask = image.data;
111                    image.data = mask
112                        .iter()
113                        .flat_map(|x| sRGB32::new(255, 255, 255, *x).as_f32().srgb_pre().as_bgra())
114                        .collect();
115                }
116                // This is in sRGB RGBA format but our texture atlas is in pre-multiplied sRGB BGRA
117                // format, so swap it
118                Content::Color => {
119                    for c in image.data.as_mut_slice().chunks_exact_mut(4) {
120                        // Pre-multiply color, then extract in BGRA form.
121                        c.copy_from_slice(
122                            &sRGB32::new(c[0], c[1], c[2], c[3])
123                                .as_f32()
124                                .srgb_pre()
125                                .as_bgra(),
126                        );
127                    }
128                }
129                Content::SubpixelMask => {
130                    // TODO: wide doesn't implement SSE shuffle instructions yet, which could
131                    // potentially be faster here
132                    let len = image.data.len() / 4;
133                    let slice = image.data.as_mut_slice();
134                    for i in 0..len {
135                        let idx = i * 4;
136                        slice.swap(idx, idx + 2);
137                        // Don't pre-multiply this because it's already a mask
138                    }
139                }
140            }
141
142            atlas.queue_data(
143                &image.data,
144                &region,
145                queue,
146                image.placement.width,
147                image.placement.height,
148            );
149        }
150
151        if let Some(mut old) = glyphs.insert(
152            key,
153            GlyphRegion {
154                offset: [image.placement.left, image.placement.top],
155                region,
156            },
157        ) {
158            atlas.destroy(&mut old.region);
159        }
160
161        Ok(())
162    }
163
164    pub fn prepare_glyph(
165        x: i32,
166        y: i32,
167        line_y: f32,
168        scale_factor: f32,
169        color: cosmic_text::Color,
170        bounds_min_x: i32,
171        bounds_min_y: i32,
172        bounds_max_x: i32,
173        bounds_max_y: i32,
174        glyph: &GlyphRegion,
175    ) -> Result<Option<super::compositor::Data>, Error> {
176        if glyph.region.uv.area() == 0 {
177            return Ok(None);
178        }
179        //let atlas_min = region.uv.min;
180
181        let mut x = x + glyph.offset[0];
182        let mut y = (line_y * scale_factor).round() as i32 + y - glyph.offset[1];
183
184        let mut u = glyph.region.uv.min.x;
185        let mut v = glyph.region.uv.min.y;
186
187        let mut width = glyph.region.uv.width();
188        let mut height = glyph.region.uv.height();
189
190        // Starts beyond right edge or ends beyond left edge
191        let max_x = x + width;
192        if x > bounds_max_x || max_x < bounds_min_x {
193            return Ok(None);
194        }
195
196        // Starts beyond bottom edge or ends beyond top edge
197        let max_y = y + height;
198        if y > bounds_max_y || max_y < bounds_min_y {
199            return Ok(None);
200        }
201
202        // Clip left edge
203        if x < bounds_min_x {
204            let right_shift = bounds_min_x - x;
205
206            x = bounds_min_x;
207            width = max_x - bounds_min_x;
208            u += right_shift;
209        }
210
211        // Clip right edge
212        if x + width > bounds_max_x {
213            width = bounds_max_x - x;
214        }
215
216        // Clip top edge
217        if y < bounds_min_y {
218            let bottom_shift = bounds_min_y - y;
219
220            y = bounds_min_y;
221            height = max_y - bounds_min_y;
222            v += bottom_shift;
223        }
224
225        // Clip bottom edge
226        if y + height > bounds_max_y {
227            height = bounds_max_y - y;
228        }
229
230        Ok(Some(super::compositor::Data {
231            pos: [x as f32, y as f32].into(),
232            dim: [width as f32, height as f32].into(),
233            uv: [u as f32, v as f32].into(),
234            uvdim: [width as f32, height as f32].into(),
235            color: u32::from_be_bytes(color.as_rgba()),
236            rotation: 0.0,
237            flags: DataFlags::new().with_tex(glyph.region.index).into(),
238            ..Default::default()
239        }))
240    }
241
242    fn evaluate(
243        buffer: &cosmic_text::Buffer,
244        pos: crate::PxPoint,
245        scale: f32,
246        mut bounds: PxRect,
247        color: cosmic_text::Color,
248        compositor: &mut super::compositor::CompositorView<'_>,
249        font_system: &mut FontSystem,
250        glyphs: &mut GlyphCache,
251        device: &wgpu::Device,
252        queue: &wgpu::Queue,
253        atlas: &mut Atlas,
254        cache: &mut ScaleContext,
255    ) -> Result<(), Error> {
256        bounds = bounds.intersect(compositor.current_clip());
257        let bounds_top = bounds.topleft().y as i32;
258        let bounds_bottom = bounds.bottomright().y as i32;
259        let bounds_min_x = (bounds.topleft().x as i32).max(0);
260        let bounds_min_y = bounds_top.max(0);
261        let bounds_max_x = bounds.bottomright().x as i32;
262        let bounds_max_y = bounds_bottom;
263
264        let is_run_visible = |run: &cosmic_text::LayoutRun| {
265            let start_y_physical = (pos.y + (run.line_top * scale)) as i32;
266            let end_y_physical = start_y_physical + (run.line_height * scale) as i32;
267
268            start_y_physical <= bounds_max_y && bounds_top <= end_y_physical
269        };
270
271        let layout_runs = buffer
272            .layout_runs()
273            .skip_while(|run| !is_run_visible(run))
274            .take_while(is_run_visible);
275
276        for run in layout_runs {
277            for glyph in run.glyphs.iter() {
278                let physical_glyph = glyph.physical((pos.x, pos.y), scale);
279
280                let glyphcolor = match glyph.color_opt {
281                    Some(some) => some,
282                    None => color,
283                };
284
285                Self::write_glyph(
286                    physical_glyph.cache_key,
287                    font_system,
288                    glyphs,
289                    device,
290                    queue,
291                    atlas,
292                    cache,
293                )?;
294
295                if let Some(data) = Self::prepare_glyph(
296                    physical_glyph.x,
297                    physical_glyph.y,
298                    run.line_y,
299                    scale,
300                    glyphcolor,
301                    bounds_min_x,
302                    bounds_min_y,
303                    bounds_max_x,
304                    bounds_max_y,
305                    Self::get_glyph(physical_glyph.cache_key, glyphs)
306                        .ok_or(Error::GlyphCacheFailure)?,
307                )? {
308                    compositor.preprocessed(data);
309                }
310            }
311        }
312
313        Ok(())
314    }
315}
316
317impl super::Renderable for Instance {
318    fn render(
319        &self,
320        area: PxRect,
321        driver: &crate::graphics::Driver,
322        compositor: &mut CompositorView<'_>,
323    ) -> Result<(), Error> {
324        let padding = self.padding.get();
325
326        Self::evaluate(
327            &self.text_buffer.borrow(),
328            area.topleft().add_size(&padding.topleft()),
329            1.0,
330            area,
331            cosmic_text::Color::rgb(255, 255, 255),
332            compositor,
333            &mut driver.font_system.write(),
334            &mut driver.glyphs.write(),
335            &driver.device,
336            &driver.queue,
337            &mut driver.atlas.write(),
338            &mut driver.swash_cache.write(),
339        )
340    }
341}