guido 0.12.0

A reactive Rust GUI library using wgpu for Wayland layer shell widgets
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
//! Glyph coverage as a texture: the shape a frosted text cuts out of its
//! background.
//!
//! A container's backdrop blur is masked by a rounded rectangle, which the
//! composite shader can evaluate from four radii. Glyphs have no such formula —
//! the only thing that knows their shape is the rasterizer. So a text that asks
//! for a backdrop blur is shaped once into a texture, in white on nothing, and
//! the composite reads its alpha where it would otherwise evaluate an SDF.
//!
//! The mask is rasterized in the text's **own space** — the one the layout box
//! is written in, before any transform — and the composite carries each
//! fragment back into that space to find its texel, so the coverage is turned
//! and stretched with the letters rather than laid over them square. What the
//! transform decides is only where the mask is read, never how it is made, so
//! one rasterization serves a whole animation.
//!
//! Shaping mirrors whatever will draw the glyphs over the frost exactly — same
//! metrics, same width, same offset — because two shapings that disagree break
//! their lines in different places, and the frost then lands beside a letter
//! that wrapped elsewhere. Every path shapes in the width the text was laid out
//! in and places it with the same offset, both from [`text`](super::text); what
//! differs between [`text`](super::text), which draws an untransformed text,
//! and [`text_quad`](super::text_quad), which draws a transformed one, is the
//! height an uncut text is given — 50 logical pixels at least through one, the
//! box and a margin through the other. So the caller hands the height in rather
//! than this module guessing which is about to be used.
//!
//! Colour is deliberately absent from the cache key: the frost of a white label
//! and a red one is the same hole.

use std::cell::Cell;
use std::rc::Rc;

use glyphon::{
    Cache, Color as GlyphonColor, ColorMode, FontSystem, Resolution, SwashCache, TextArea,
    TextAtlas, TextBounds, TextRenderer, Viewport,
};
use rustc_hash::FxHashMap;
use wgpu::{Device, MultisampleState, Queue, TextureFormat};

use crate::render_stats::{self, Pipeline};
use crate::widgets::font::FontWeight;
use crate::widgets::{FontFamily, LineHeight, TextAlign};

/// Masks kept before the unused ones are dropped.
///
/// A surface frosting more than a handful of texts at once is not the case this
/// is for — each one costs a pass break — so the ceiling is low and the sweep
/// keeps whatever the current frame touched.
const MAX_CACHED: usize = 32;

/// Refuse a mask larger than this per side, in texels. The logical text that
/// allows is this over the density, so a HiDPI screen halves it and a transform
/// halves it again. A frost is an effect on a label, and an accidental one over
/// a full-screen text would allocate a second framebuffer with no warning.
const MAX_SIDE: u32 = 2048;

/// What a mask has to be rasterized from.
pub struct MaskSpec<'a> {
    pub text: &'a str,
    pub font_size: f32,
    pub font_family: FontFamily,
    pub font_weight: FontWeight,
    pub line_height: LineHeight,
    /// Extra advance after every glyph, in logical pixels.
    pub letter_spacing: f32,
    /// The lines the text is cut to, when it is cut.
    pub fit: Option<super::text_measurer::LineFit>,
    /// Where each line sits across the buffer, as the letters over the frost
    /// have it.
    pub align: TextAlign,
    /// The height to shape an uncut text in, in the same texels as everything
    /// else here.
    ///
    /// Handed in rather than derived, because the rule belongs to whichever
    /// path will draw the glyphs and there are two of them — see the module
    /// documentation.
    pub buffer_height: f32,
    /// The width of the text's box, in texels: what the glyphs are placed
    /// across, by [`paint_offset`](super::text::paint_offset), as the letters
    /// over the frost are.
    pub box_width: f32,
    /// Mask size in texels: the frame the composite reads it over.
    pub size: (u32, u32),
    /// Where the glyph origin sits inside that frame, in texels — the slack the
    /// frame carries on every side so a descender or a contour has somewhere to
    /// reach.
    pub offset: (f32, f32),
    /// Texels per logical pixel: the surface scale, and
    /// [`TEXT_SUPERSAMPLE`](super::constants::TEXT_SUPERSAMPLE) times it when
    /// the transform stretches the mask over more pixels than it has texels.
    ///
    /// Whether it stretches, never by how much — so the mask a scale animation
    /// reads is rasterized twice over its length rather than once a frame.
    pub density: f32,
}

#[derive(PartialEq, Eq, Hash)]
struct MaskKey {
    text: String,
    font_size_bits: u32,
    weight: u16,
    family: FontFamily,
    line_height: (u8, u32),
    letter_spacing_bits: u32,
    align: TextAlign,
    width: u32,
    height: u32,
    /// Truncated, and in the key because it decides which lines of an uncut
    /// text are laid out: two masks alike in every other field can still be
    /// shaped differently.
    buffer_height: u32,
    /// By its bits, because it places the glyphs and a mask has to sit exactly
    /// under them.
    box_width_bits: u32,
    /// The glyph origin inside the frame, in quarter texels. Quantised because
    /// it follows a stroke width, and a mask per unique float would never hit.
    offset: (i32, i32),
    /// The cut, which decides which letters the mask holds at all.
    fit: Option<super::text_measurer::LineFitKey>,
}

struct CachedMask {
    #[allow(dead_code)] // Held to keep the view alive.
    texture: wgpu::Texture,
    view: Rc<wgpu::TextureView>,
    last_used: Cell<u64>,
}

/// The glyphon side of a path that rasterizes one text at a time into a
/// texture of its own — a frosted text's mask here, a turned text's texture in
/// [`text_quad`](super::text_quad) — built on the first such text and not
/// before.
///
/// A `FontSystem` scans the machine's fonts to construct, and the atlas is two
/// textures before it holds a glyph: not a cost to put on every app for paths
/// most of them never take.
pub(super) struct Shaper {
    pub(super) font_system: FontSystem,
    pub(super) swash_cache: SwashCache,
    #[allow(dead_code)] // Held for the atlas and viewport.
    cache: Cache,
    pub(super) atlas: TextAtlas,
    pub(super) text_renderer: TextRenderer,
    pub(super) viewport: Viewport,
}

impl Shaper {
    pub(super) fn new(device: &Device, queue: &Queue, format: TextureFormat) -> Self {
        let mut font_system = FontSystem::new();
        for data in crate::get_registered_fonts() {
            font_system
                .db_mut()
                .load_font_source(glyphon::fontdb::Source::Binary(data));
        }
        let cache = Cache::new(device);
        let mut atlas = TextAtlas::with_color_mode(device, queue, &cache, format, ColorMode::Web);
        let text_renderer =
            TextRenderer::new(&mut atlas, device, MultisampleState::default(), None);
        let viewport = Viewport::new(device, &cache);

        Self {
            font_system,
            swash_cache: SwashCache::new(),
            cache,
            atlas,
            text_renderer,
            viewport,
        }
    }

    /// Draw one text into `view`, cleared first, and submit it. Answers
    /// whether the text could be prepared at all.
    ///
    /// Ends by trimming: this atlas draws one text at a time, and that text is
    /// drawn now, so its glyphs stop being in use and the next text may evict
    /// them rather than grow the atlas around them. After the submit and not
    /// before, because the next text's glyph uploads would otherwise overwrite
    /// slots this one's pass still reads.
    pub(super) fn draw(
        &mut self,
        device: &Device,
        queue: &Queue,
        area: glyphon::TextArea<'_>,
        view: &wgpu::TextureView,
        label: &str,
    ) -> bool {
        if let Err(e) = self.text_renderer.prepare(
            device,
            queue,
            &mut self.font_system,
            &mut self.atlas,
            &self.viewport,
            [area],
            &mut self.swash_cache,
        ) {
            log::error!("{label} prepare failed: {e:?}");
            return false;
        }

        let mut encoder =
            device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some(label) });
        {
            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some(label),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
                        store: wgpu::StoreOp::Store,
                    },
                    depth_slice: None,
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
                multiview_mask: None,
            });
            match self
                .text_renderer
                .render(&self.atlas, &self.viewport, &mut pass)
            {
                Ok(()) => render_stats::record_draw_calls(Pipeline::Text, 1),
                Err(e) => log::error!("{label} render failed: {e:?}"),
            }
        }
        queue.submit(std::iter::once(encoder.finish()));
        self.atlas.trim();
        true
    }
}

/// Rasterizes glyph coverage, and remembers it.
pub struct TextMaskRenderer {
    shaper: Option<Shaper>,
    format: TextureFormat,
    masks: FxHashMap<MaskKey, Rc<CachedMask>>,
    frame_gen: u64,
}

impl TextMaskRenderer {
    pub fn new(format: TextureFormat) -> Self {
        Self {
            shaper: None,
            format,
            masks: FxHashMap::default(),
            frame_gen: 0,
        }
    }

    /// Note a new frame, and drop masks nothing has asked for in a while.
    pub fn begin_frame(&mut self) {
        self.frame_gen += 1;
        if self.masks.len() > MAX_CACHED {
            let current = self.frame_gen;
            self.masks
                .retain(|_, mask| mask.last_used.get() + 1 >= current);
        }
    }

    /// The coverage of `spec`, rasterized or recalled.
    ///
    /// Renders and submits on its own encoder, so the mask is ready before the
    /// frame that samples it is submitted.
    pub fn mask(
        &mut self,
        device: &Device,
        queue: &Queue,
        spec: &MaskSpec<'_>,
    ) -> Option<Rc<wgpu::TextureView>> {
        let (width, height) = spec.size;
        if spec.text.is_empty() || width == 0 || height == 0 {
            return None;
        }
        if width > MAX_SIDE || height > MAX_SIDE {
            log::warn!(
                "text backdrop blur skipped: mask would be {width}x{height}, over the {MAX_SIDE} limit"
            );
            return None;
        }

        let font_size = spec.font_size * spec.density;
        let weight = if spec.font_weight == FontWeight::default() {
            FontWeight::NORMAL
        } else {
            spec.font_weight
        };
        let key = MaskKey {
            text: spec.text.to_owned(),
            font_size_bits: font_size.to_bits(),
            weight: weight.0,
            family: spec.font_family,
            line_height: spec.line_height.key(),
            letter_spacing_bits: spec.letter_spacing.to_bits(),
            align: spec.align,
            width,
            height,
            buffer_height: spec.buffer_height as u32,
            box_width_bits: spec.box_width.to_bits(),
            offset: (
                (spec.offset.0 * 4.0).round() as i32,
                (spec.offset.1 * 4.0).round() as i32,
            ),
            fit: spec.fit.map(|fit| fit.key()),
        };

        if let Some(cached) = self.masks.get(&key) {
            cached.last_used.set(self.frame_gen);
            return Some(Rc::clone(&cached.view));
        }

        let format = self.format;
        let shaper = self
            .shaper
            .get_or_insert_with(|| Shaper::new(device, queue, format));

        // Shaped the way the on-screen text is shaped, or the hole would not be
        // the shape of the letters that land in it.
        let buffer = super::text::shape(
            &mut shaper.font_system,
            spec.text,
            font_size,
            spec.font_family,
            spec.font_weight,
            spec.line_height,
            spec.letter_spacing,
            spec.align,
            (Some(spec.box_width), Some(spec.buffer_height)),
            spec.fit,
            spec.density,
        );

        let texture = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("Text Mask"),
            size: wgpu::Extent3d {
                width,
                height,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format,
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
            view_formats: &[],
        });
        let view = texture.create_view(&Default::default());

        shaper.viewport.update(queue, Resolution { width, height });

        let area = TextArea {
            buffer: &buffer,
            left: spec.offset.0 + super::text::paint_offset(&buffer, spec.box_width, spec.align),
            top: spec.offset.1,
            scale: 1.0,
            bounds: TextBounds {
                left: 0,
                top: 0,
                right: width as i32,
                bottom: height as i32,
            },
            // Opaque white: what is wanted is coverage, and a translucent text
            // would otherwise thin its own hole.
            default_color: GlyphonColor::rgb(255, 255, 255),
            custom_glyphs: &[],
        };

        if !shaper.draw(device, queue, area, &view, "Text Mask") {
            return None;
        }

        let view = Rc::new(view);
        self.masks.insert(
            key,
            Rc::new(CachedMask {
                texture,
                view: Rc::clone(&view),
                last_used: Cell::new(self.frame_gen),
            }),
        );
        Some(view)
    }
}

/// The glyph atlas behind the masks holds one text's glyphs, not every
/// text's — see `the_glyph_atlas_holds_a_frame_not_a_history` in `text.rs`, whose pages this
/// frosts.
#[cfg(test)]
mod the_glyph_atlas_holds_a_text_not_a_history {
    use super::{MaskSpec, Shaper, TextMaskRenderer};
    use crate::renderer::GpuContext;
    use crate::renderer::text::the_glyph_atlas_holds_a_frame_not_a_history::{
        holds_one_page, page,
    };
    use crate::transform::Transform;

    /// The atlas a fresh renderer holds after frosting `frames`, one frame
    /// each — and only the atlas: the masks it rasterized are given back
    /// before the device is asked.
    fn atlas_after(gpu: &GpuContext, frames: &[String]) -> isize {
        let before = gpu.texture_bytes();
        let format = wgpu::TextureFormat::Rgba8UnormSrgb;
        let mut renderer = TextMaskRenderer::new(format);
        let mut shaper = Shaper::new(&gpu.device, &gpu.queue, format);
        shaper.font_system = crate::renderer::text::vendored_font_system();
        renderer.shaper = Some(shaper);
        for text in frames {
            renderer.begin_frame();
            let entry = page(text, Transform::IDENTITY);
            let spec = MaskSpec {
                text: &entry.text,
                font_size: entry.font_size,
                font_family: entry.font_family,
                font_weight: entry.font_weight,
                line_height: entry.line_height,
                letter_spacing: entry.letter_spacing,
                fit: None,
                align: entry.align,
                buffer_height: entry.rect.height,
                box_width: entry.rect.width,
                size: (entry.rect.width as u32, entry.rect.height as u32),
                offset: (0.0, 0.0),
                density: 1.0,
            };
            assert!(
                renderer.mask(&gpu.device, &gpu.queue, &spec).is_some(),
                "a frame's mask was not made"
            );
        }
        renderer.masks.clear();
        gpu.texture_bytes() - before
    }

    #[test]
    fn a_frosted_text_that_changes_every_frame_needs_no_more_atlas_than_one_frame() {
        holds_one_page(atlas_after);
    }
}