git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/backend/frame.rs (28.9K)

  1 //! Building a frame: everything between "the window needs a frame" and "hand
  2 //! the renderer its data" that knows nothing of the window system. The app is
  3 //! asked for its display list, damage, custom vertices and overlays; widgets
  4 //! are shaped against the glyph pass's font system; the list is tessellated
  5 //! and its text gathered; popovers are collected for the text-occlusion
  6 //! clamp. What comes out is a [`BuiltFrame`] — plain data the renderer draws
  7 //! — and a shell presents it however its window system presents.
  8 //!
  9 //! The Wayland shell's half (`EngineState::render`) is what is left: the
 10 //! grid patch and input region, uploading the glyphs, the extent gate and
 11 //! buffer scale, the frame callback, `stage_renderer` and the draw. Order
 12 //! between the two halves is the order the frame always had — the app's
 13 //! damage is taken here even when the shell then skips the present.
 14 
 15 use cosmic_text::FontSystem;
 16 
 17 use super::app::{Application, LogicalSize};
 18 use super::tessellate::{quad_vertices, tessellate_display_list, DlBatch, Vertex};
 19 use super::text::{collect_dl_text, dl_text_spans, TextBounds};
 20 use crate::draw::{Batch2D, Frame2D, ImageQuad, TextSpan};
 21 use super::text::DlText;
 22 
 23 /// One frame, built and owned: the renderer's [`Frame2D`] is a borrow of it
 24 /// ([`frame2d`](Self::frame2d)). Its display-list text is shaped into the
 25 /// `text_items` that [`build_frame`] was handed, and lent to the glyph pass
 26 /// through [`text_spans`](Self::text_spans).
 27 pub struct BuiltFrame {
 28     pub verts: Vec<Vertex>,
 29     pub batches: Vec<Batch2D>,
 30     /// Drawn after the text pass.
 31     pub overlay_verts: Vec<Vertex>,
 32     pub images: Vec<ImageQuad>,
 33     pub plate_features: Vec<[f32; 12]>,
 34     /// Linear, alpha as given.
 35     pub clear_color: [f32; 4],
 36     /// The part of the surface that changed, physical px; `None` = all of it.
 37     pub damage: Option<(u32, u32, u32, u32)>,
 38     /// The app's text is the display list's (`Application::display_list_text`).
 39     /// When false the app stages the renderer's text itself and the glyph
 40     /// pass must be left alone.
 41     pub dl_text: bool,
 42     /// Popover rects (and the in-window context menu) that page text is
 43     /// clamped away from, logical px, in stacking order.
 44     overlay_rects: Vec<(f32, f32, f32, f32)>,
 45     scale: f32,
 46     physical: (u32, u32),
 47 }
 48 
 49 impl BuiltFrame {
 50     /// The display-list text as glyph spans, borrowing `items` — the vector
 51     /// [`build_frame`] shaped this frame's text into.
 52     pub fn text_spans<'a>(&self, items: &'a [DlText]) -> Vec<TextSpan<'a>> {
 53         let (pw, ph) = self.physical;
 54         let bounds = TextBounds { left: 0, top: 0, right: pw as i32, bottom: ph as i32 };
 55         // All text is display-list text: each Text prim with the default
 56         // mapping (scale + surface clamp) plus the popover-occlusion clamp
 57         // against the app's registered popovers.
 58         dl_text_spans(items, self.scale, bounds, &self.overlay_rects)
 59     }
 60 
 61     /// The frame as the renderer takes it.
 62     pub fn frame2d(&self) -> Frame2D<'_> {
 63         Frame2D {
 64             verts: &self.verts,
 65             batches: &self.batches,
 66             overlay_verts: &self.overlay_verts,
 67             images: &self.images,
 68             plate_features: &self.plate_features,
 69             clear_color: self.clear_color,
 70             damage: self.damage,
 71         }
 72     }
 73 }
 74 
 75 /// Build the app's frame at `size` and `scale`.
 76 ///
 77 /// `damage_owed` is the shell's: true when the last built frame was not
 78 /// presented, so this one repaints everything. It is set true here; the
 79 /// shell clears it once a frame is actually presented. `text_items` is where
 80 /// the display-list text is shaped and held, for [`BuiltFrame::text_spans`].
 81 pub fn build_frame<A: Application>(
 82     app: &mut A,
 83     fs: &mut FontSystem,
 84     size: LogicalSize,
 85     scale: f64,
 86     damage_owed: &mut bool,
 87     text_items: &mut Vec<DlText>,
 88 ) -> BuiltFrame {
 89     let (logical_w, logical_h) = (size.width, size.height);
 90 
 91     // The editing widget reports its caret as it paints (`ime::report_caret`):
 92     // where an input method's candidates go, and whether text is wanted.
 93     crate::ime::begin_frame();
 94 
 95     // 0. Shape every registered widget against the SAME FontSystem the glyph pass draws
 96     // with, before the app builds its frame. A widget's caret/selection/click→index math
 97     // reads per-glyph advances its `prepare_text` records; nothing else calls it on the
 98     // display-list path (the paint walk is `&dyn`, and apps were left to remember —
 99     // cce-list, cce-secrets, and the reference DemoApp all forgot, so their carets fell
100     // back to `measure_text_width("M")`, an inked extent that drifts off the glyphs).
101     // The flat path shapes in `layout::render_widget`; apps that hand-shape still work —
102     // their call and this one hit the same shaped-buffer cache. Through the shared
103     // `ui_context`, which every app with widgets answers (several have no `_mut`): the
104     // widgets live in the context's own allocations, reached by their raw roots, and no
105     // reference to one is live while it is shaped.
106     {
107         let ptrs: Vec<*mut (dyn crate::widget::WidgetHost + 'static)> = app
108             .ui_context()
109             .map(|ctx| ctx.tree.iter_registered().map(|(_, p)| p).collect())
110             .unwrap_or_default();
111         for ptr in ptrs {
112             // SAFETY: a widget the context holds and has not lent out, reached by its root;
113             // the borrow of the context that found it has ended.
114             unsafe { (*ptr).prepare_text(fs) };
115         }
116     }
117 
118     // 1. The frame's geometry IS the app's display list — the single paint path. Tessellated
119     // below as one batched, GPU-scissor-clipped pass. An app that draws nothing returns
120     // `None`, giving an empty frame (the legacy view*/tuple-wrapping path is gone).
121     let dl = app
122         .display_list(size, scale)
123         .unwrap_or_else(|| crate::scene::paint::PaintCtx::new().finish());
124     crate::ime::end_frame();
125     // Taken with the display list it describes. A frame that took the
126     // app's damage and then was not presented owes those pixels, so the
127     // next one that is presented repaints everything.
128     let app_damage = app.take_damage(size, scale);
129     let damage = match (app_damage, *damage_owed) {
130         (Some((x, y, w, h)), false) => {
131             // Outward to whole physical pixels, plus one for an
132             // antialiased edge.
133             let s = scale as f32;
134             let x0 = ((x * s).floor() - 1.0).max(0.0);
135             let y0 = ((y * s).floor() - 1.0).max(0.0);
136             let x1 = ((x + w) * s).ceil() + 1.0;
137             let y1 = ((y + h) * s).ceil() + 1.0;
138             Some((x0 as u32, y0 as u32, (x1 - x0).max(0.0) as u32, (y1 - y0).max(0.0) as u32))
139         }
140         _ => None,
141     };
142     *damage_owed = true;
143 
144     // 1a. Phase 6 display-list text: shape the list's Text prims through the shared buffer
145     // cache and hold them for the glyph pass (the TextSpans the shell builds borrow these).
146     // Clip = the paint walk's item clip ∩ the prim's own bounds, in logical space.
147     text_items.clear();
148     let dl_text = app.display_list_text();
149     if dl_text {
150         collect_dl_text(fs, &dl, text_items);
151     }
152 
153     let (mut verts, mut dl_batches, dl_images, plate_features) =
154         tessellate_display_list(&dl, logical_w, logical_h, scale as f32);
155     // A pending height-field export (`CCE_HEIGHTMAP`, or an app's
156     // `scene::heightfield::request`): the plates of THIS frame, sampled
157     // as the geometry the shader is about to shade.
158     if let Some(req) = crate::scene::heightfield::take_request() {
159         let s = scale as f32;
160         let (pw, ph) = ((logical_w * s).round() as usize, (logical_h * s).round() as usize);
161         let hf = crate::scene::heightfield::HeightField::from_frame(&dl_batches, &plate_features, pw, ph, s);
162         let (lo, hi) = hf.range_px();
163         match crate::scene::heightfield::export_png(&hf, &req.path, req.mm_per_sample) {
164             Ok(()) => log::info!(
165                 "[heightfield] wrote {} ({}x{} px, {:.3}..{:.3} mm, metric {})",
166                 req.path.display(), pw, ph, lo / hf.px_per_mm, hi / hf.px_per_mm, hf.source.as_str()
167             ),
168             Err(e) => log::warn!("[heightfield] export to {} failed: {e}", req.path.display()),
169         }
170     }
171     // custom_vertices (e.g. graph geometry) is appended as a final unclipped batch drawn on top.
172     let pre_custom = verts.len() as u32;
173     app.custom_vertices(&mut verts, size, scale);
174     if (verts.len() as u32) > pre_custom {
175         dl_batches.push(DlBatch {
176             scissor: None,
177             clip_rrect: None,
178             start: pre_custom,
179             end: verts.len() as u32,
180             plate: None,
181             blur_behind: false,
182         });
183     }
184 
185     // 1b. Overlay quads (drawn after the text pass).
186     let mut overlay_quads = Vec::new();
187     app.overlay_quads(&mut overlay_quads, size, scale);
188     let mut overlay_verts = Vec::new();
189     for &(qx, qy, qw, qh, qc) in &overlay_quads {
190         overlay_verts.extend(quad_vertices(qx, qy, qw, qh, logical_w, logical_h, qc));
191     }
192 
193     // 2. What page text is clamped away from: the app's open popovers…
194     let scale_f32 = scale as f32;
195     let mut overlay_rects: Vec<(f32, f32, f32, f32)> = Vec::new();
196     if let Some(ctx) = app.ui_context() {
197         for &pop_id in &ctx.active_popovers {
198             if let Some((x, y, w, h)) = ctx.get_widget(pop_id).and_then(|w| w.popover_rect()) {
199                 let (dx, dy) = app.popover_offset(pop_id);
200                 overlay_rects.push((x + dx, y + dy, w, h));
201             }
202         }
203     }
204     // …and the global context menu, which draws into the app's display list,
205     // so it gets the same occlusion: the menu rect clamps list text beneath,
206     // and the menu's own labels are exempt because they carry bounds equal to
207     // the rect. Hosted in its popup surface, the menu covers the window from
208     // above and nothing of it is in the list.
209     if crate::widget::context_menu::is_visible() && !crate::widget::context_menu::is_hosted() {
210         overlay_rects.push((
211             crate::widget::context_menu::x(),
212             crate::widget::context_menu::y(),
213             crate::widget::context_menu::w(),
214             crate::widget::context_menu::h(),
215         ));
216     }
217 
218     // 3. Images and batches in physical px.
219     let images: Vec<ImageQuad> = dl_images
220         .iter()
221         .map(|di| ImageQuad {
222             image: di.image,
223             rect: (
224                 di.rect.x * scale_f32,
225                 di.rect.y * scale_f32,
226                 di.rect.width * scale_f32,
227                 di.rect.height * scale_f32,
228             ),
229             alpha: di.alpha,
230             z_before: di.at,
231             clip: di.clip.map(|c| {
232                 (
233                     (c.x * scale_f32).max(0.0) as u32,
234                     (c.y * scale_f32).max(0.0) as u32,
235                     (c.width * scale_f32) as u32,
236                     (c.height * scale_f32) as u32,
237                 )
238             }),
239         })
240         .collect();
241 
242     let batches = super::tessellate::dl_batches_2d(&dl_batches, scale_f32);
243 
244     let cc = app.clear_color();
245     let clear_color = [cc[0].powf(2.2), cc[1].powf(2.2), cc[2].powf(2.2), cc[3]];
246 
247     BuiltFrame {
248         verts,
249         batches,
250         overlay_verts,
251         images,
252         plate_features,
253         clear_color,
254         damage,
255         dl_text,
256         overlay_rects,
257         scale: scale_f32,
258         physical: ((logical_w * scale_f32) as u32, (logical_h * scale_f32) as u32),
259     }
260 }
261 
262 /// What a presented frame drew, kept so the next frame can be diffed against
263 /// it ([`derive_damage`]). One per window; physical px throughout.
264 #[derive(Default)]
265 pub struct FrameRecord {
266     prev: Option<FrameSig>,
267 }
268 
269 /// A box as (x0, y0, x1, y1), physical px.
270 type PxBox = (i32, i32, i32, i32);
271 
272 struct FrameSig {
273     physical: (u32, u32),
274     clear: [u32; 4],
275     features: u64,
276     batches: Vec<(u64, Option<PxBox>)>,
277     texts: Vec<(u64, Option<PxBox>)>,
278     images: Vec<(u64, Option<PxBox>, u32)>,
279     overlay: (u64, Option<PxBox>),
280 }
281 
282 fn hash_of(f: impl FnOnce(&mut std::collections::hash_map::DefaultHasher)) -> u64 {
283     use std::hash::Hasher;
284     let mut h = std::collections::hash_map::DefaultHasher::new();
285     f(&mut h);
286     h.finish()
287 }
288 
289 fn union_box(a: Option<PxBox>, b: Option<PxBox>) -> Option<PxBox> {
290     match (a, b) {
291         (Some(a), Some(b)) => Some((a.0.min(b.0), a.1.min(b.1), a.2.max(b.2), a.3.max(b.3))),
292         (a, b) => a.or(b),
293     }
294 }
295 
296 fn clip_box(b: Option<PxBox>, clip: Option<(u32, u32, u32, u32)>) -> Option<PxBox> {
297     let b = b?;
298     let Some((cx, cy, cw, ch)) = clip else { return Some(b) };
299     let c = (cx as i32, cy as i32, (cx + cw) as i32, (cy + ch) as i32);
300     let r = (b.0.max(c.0), b.1.max(c.1), b.2.min(c.2), b.3.min(c.3));
301     (r.0 < r.2 && r.1 < r.3).then_some(r)
302 }
303 
304 fn verts_box(verts: &[Vertex], (pw, ph): (u32, u32)) -> Option<PxBox> {
305     let (mut x0, mut y0, mut x1, mut y1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
306     for v in verts {
307         let px = (v.position[0] + 1.0) * 0.5 * pw as f32;
308         let py = (1.0 - v.position[1]) * 0.5 * ph as f32;
309         x0 = x0.min(px);
310         y0 = y0.min(py);
311         x1 = x1.max(px);
312         y1 = y1.max(py);
313     }
314     (x0 <= x1).then(|| ((x0.floor() as i32) - 2, (y0.floor() as i32) - 2, (x1.ceil() as i32) + 2, (y1.ceil() as i32) + 2))
315 }
316 
317 impl FrameSig {
318     fn of(frame: &BuiltFrame, texts: &[DlText]) -> Self {
319         use std::hash::Hash;
320         let physical = frame.physical;
321         let batches = frame
322             .batches
323             .iter()
324             .map(|b| {
325                 let verts = frame.verts.get(b.start as usize..b.end as usize).unwrap_or(&[]);
326                 let key = hash_of(|h| {
327                     bytemuck::cast_slice::<Vertex, u8>(verts).hash(h);
328                     b.scissor.hash(h);
329                     b.clip_rrect.map(|c| c.map(f32::to_bits)).hash(h);
330                     b.blur_behind.hash(h);
331                     if let Some(p) = &b.plate {
332                         format!("{p:?}").hash(h);
333                     }
334                 });
335                 (key, clip_box(verts_box(verts, physical), b.scissor))
336             })
337             .collect();
338         let s = frame.scale;
339         let texts = texts
340             .iter()
341             .map(|t| {
342                 let m = t.buffer.metrics();
343                 let (mut w, mut bottom) = (0.0f32, 0.0f32);
344                 let key = hash_of(|h| {
345                     for line in &t.buffer.lines {
346                         line.text().hash(h);
347                     }
348                     // The shaped glyphs, not just the string: the same text
349                     // in another face (family, style, weight) is a different
350                     // picture. Hashing the string alone left a font picker's
351                     // preview showing the last face until something else
352                     // damaged the rows it sat in.
353                     for run in t.buffer.layout_runs() {
354                         w = w.max(run.line_w);
355                         bottom = bottom.max(run.line_top + run.line_height);
356                         for g in run.glyphs {
357                             (g.font_id, g.glyph_id, g.cache_key_flags, g.color_opt.map(|c| c.0)).hash(h);
358                             [g.x, g.y, g.w, g.font_size, g.x_offset, g.y_offset].map(f32::to_bits).hash(h);
359                         }
360                     }
361                     (t.x.to_bits(), t.y.to_bits(), t.color.0, m.font_size.to_bits()).hash(h);
362                     t.bounds.map(|b| b.map(f32::to_bits)).hash(h);
363                     t.clip_circle.map(|c| c.map(f32::to_bits)).hash(h);
364                     t.clip_rrect.map(|c| c.map(f32::to_bits)).hash(h);
365                 });
366                 // Generous: a glyph can overhang its advance box, and the
367                 // run's top is not the ink's.
368                 let pad = m.font_size.max(m.line_height);
369                 let (x, y) = (t.x * s, t.y * s);
370                 let b = (
371                     (x - pad).floor() as i32,
372                     (y - pad - m.line_height).floor() as i32,
373                     (x + w + pad).ceil() as i32,
374                     (y + bottom + pad).ceil() as i32,
375                 );
376                 (key, Some(b))
377             })
378             .collect();
379         let images = frame
380             .images
381             .iter()
382             .map(|q| {
383                 let key = hash_of(|h| {
384                     (q.image, q.rect.0.to_bits(), q.rect.1.to_bits(), q.rect.2.to_bits(), q.rect.3.to_bits()).hash(h);
385                     (q.alpha.to_bits(), q.z_before, q.clip).hash(h);
386                 });
387                 let r = q.rect;
388                 let b = Some((r.0.floor() as i32 - 1, r.1.floor() as i32 - 1, (r.0 + r.2).ceil() as i32 + 1, (r.1 + r.3).ceil() as i32 + 1));
389                 (key, clip_box(b, q.clip), q.image)
390             })
391             .collect();
392         let overlay = (
393             hash_of(|h| bytemuck::cast_slice::<Vertex, u8>(&frame.overlay_verts).hash(h)),
394             verts_box(&frame.overlay_verts, physical),
395         );
396         FrameSig {
397             physical,
398             clear: frame.clear_color.map(f32::to_bits),
399             features: hash_of(|h| frame.plate_features.iter().for_each(|f| f.map(f32::to_bits).hash(h))),
400             batches,
401             texts,
402             images,
403             overlay,
404         }
405     }
406 }
407 
408 /// The boxes that differ between two runs of (key, box), old and new: the
409 /// common prefix and suffix are unchanged, and everything between them —
410 /// inserted, removed or altered — is damage, where it was AND where it is.
411 fn diff_runs<T>(old: &[T], new: &[T], key: impl Fn(&T) -> u64, bx: impl Fn(&T) -> Option<PxBox>) -> Option<PxBox> {
412     let pre = old.iter().zip(new).take_while(|(a, b)| key(a) == key(b)).count();
413     let suf = old[pre..].iter().rev().zip(new[pre..].iter().rev()).take_while(|(a, b)| key(a) == key(b)).count();
414     let mut d = None;
415     for x in &old[pre..old.len() - suf] {
416         d = union_box(d, bx(x).or(Some((i32::MIN / 2, i32::MIN / 2, i32::MAX / 2, i32::MAX / 2))));
417     }
418     for x in &new[pre..new.len() - suf] {
419         d = union_box(d, bx(x).or(Some((i32::MIN / 2, i32::MIN / 2, i32::MAX / 2, i32::MAX / 2))));
420     }
421     d
422 }
423 
424 /// Damage the runner works out for itself, for an app that does not report
425 /// its own (`Application::take_damage`): what this frame drew that the last
426 /// presented one did not, by diffing the tessellated batches, the text and
427 /// the images. The renderer repaints only that (growing it around frosted
428 /// plates) and reports only it to the compositor. Until 2026-10-06 every such
429 /// frame was a full repaint — a one-button hover change repainted and
430 /// recomposited the whole window.
431 ///
432 /// Conservative: anything it cannot see a frame's difference in makes the
433 /// frame full — a new size, scale or clear colour, changed plate carves, the
434 /// first frame, an app that stages its own text, an image whose pixels were
435 /// replaced in place is damaged where it is drawn. `CCE_UI_FULL_DAMAGE=1`
436 /// turns it off.
437 pub fn derive_damage(frame: &mut BuiltFrame, record: &mut FrameRecord, texts: &[DlText], damage_owed: bool) {
438     static OFF: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
439     let off = *OFF.get_or_init(|| std::env::var_os("CCE_UI_FULL_DAMAGE").is_some());
440     let updated = crate::draw::images::take_updated_ids();
441     let sig = FrameSig::of(frame, texts);
442     let prev = record.prev.replace(sig);
443     if off || damage_owed || frame.damage.is_some() || !frame.dl_text {
444         return;
445     }
446     let (Some(prev), Some(cur)) = (prev, record.prev.as_ref()) else { return };
447     if prev.physical != cur.physical || prev.clear != cur.clear || prev.features != cur.features {
448         return;
449     }
450     let mut d = diff_runs(&prev.batches, &cur.batches, |b| b.0, |b| b.1);
451     d = union_box(d, diff_runs(&prev.texts, &cur.texts, |t| t.0, |t| t.1));
452     d = union_box(d, diff_runs(&prev.images, &cur.images, |i| i.0, |i| i.1));
453     if prev.overlay.0 != cur.overlay.0 {
454         d = union_box(d, union_box(prev.overlay.1, cur.overlay.1));
455     }
456     for (_, b, id) in &cur.images {
457         if updated.contains(id) {
458             d = union_box(d, *b);
459         }
460     }
461     let (pw, ph) = cur.physical;
462     let (x0, y0, x1, y1) = d.unwrap_or((0, 0, 1, 1));
463     let (x0, y0) = (x0.clamp(0, pw as i32), y0.clamp(0, ph as i32));
464     let (x1, y1) = (x1.clamp(x0, pw as i32), y1.clamp(y0, ph as i32));
465     if x1 - x0 >= pw as i32 && y1 - y0 >= ph as i32 {
466         return;
467     }
468     frame.damage = Some((x0 as u32, y0 as u32, (x1 - x0).max(1) as u32, (y1 - y0).max(1) as u32));
469     // The switch lives with the Vulkan renderer, which the browser build does not have.
470     #[cfg(not(target_arch = "wasm32"))]
471     if crate::vk::present_debug() {
472         eprintln!("[vk] derived damage {}x{}+{}+{} of {pw}x{ph}", x1 - x0, y1 - y0, x0, y0);
473     }
474 }
475 
476 #[cfg(test)]
477 mod tests {
478     //! The builder with no window and no GPU: a mock app, an empty font
479     //! database, and the frame's data read back.
480     use super::*;
481     use crate::backend::app::{AppSender, LogicalPosition, WindowSettings};
482     use crate::widget::{ElementState, KeyEvent, MouseButton, MouseScrollDelta};
483 
484     #[derive(Default)]
485     struct Mock {
486         damage: Option<(f32, f32, f32, f32)>,
487         custom: usize,
488         overlay: bool,
489     }
490 
491     impl Application for Mock {
492         type Message = ();
493         fn create(_: AppSender<()>) -> Self {
494             unreachable!("built directly")
495         }
496         fn settings(&self) -> WindowSettings {
497             WindowSettings { title: String::new(), app_id: "mock".into(), width: 100, height: 50, fullscreen: false, min_size: None }
498         }
499         fn update(&mut self, _: (), _: &mut bool, _: &mut bool) {}
500         fn tick(&mut self, _: f32, _: &mut bool) {}
501         fn handle_pointer_move(&mut self, _: LogicalPosition, _: &mut bool) {}
502         fn handle_mouse_input(&mut self, _: MouseButton, _: ElementState, _: LogicalPosition, _: &mut bool) -> Option<()> {
503             None
504         }
505         fn handle_mouse_wheel(&mut self, _: &MouseScrollDelta, _: LogicalPosition, _: &mut bool) {}
506         fn handle_key_input(&mut self, _: &KeyEvent, _: &mut bool) -> Option<()> {
507             None
508         }
509         fn take_damage(&mut self, _: LogicalSize, _: f64) -> Option<(f32, f32, f32, f32)> {
510             self.damage
511         }
512         fn custom_vertices(&mut self, verts: &mut Vec<Vertex>, size: LogicalSize, _: f64) {
513             for _ in 0..self.custom {
514                 verts.extend(quad_vertices(0.0, 0.0, 1.0, 1.0, size.width, size.height, [1.0; 4]));
515             }
516         }
517         fn overlay_quads(&mut self, quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, _: LogicalSize, _: f64) {
518             if self.overlay {
519                 quads.push((1.0, 2.0, 3.0, 4.0, [1.0; 4]));
520             }
521         }
522         fn clear_color(&self) -> [f32; 4] {
523             [0.5, 0.0, 1.0, 0.25]
524         }
525     }
526 
527     fn fonts() -> FontSystem {
528         FontSystem::new_with_locale_and_db(crate::locale::locale().into(), cosmic_text::fontdb::Database::new())
529     }
530 
531     const SIZE: LogicalSize = LogicalSize { width: 100.0, height: 50.0 };
532 
533     #[test]
534     fn damage_is_physical_and_owed_until_a_present_clears_it() {
535         let mut app = Mock { damage: Some((10.2, 4.0, 5.0, 3.0)), ..Mock::default() };
536         let (mut fs, mut items) = (fonts(), Vec::new());
537 
538         // Nothing owed: the app's rect, outward to whole pixels at 2x plus one.
539         let mut owed = false;
540         let f = build_frame(&mut app, &mut fs, SIZE, 2.0, &mut owed, &mut items);
541         assert_eq!(f.damage, Some((19, 7, 13, 8)));
542         assert!(owed, "a built frame owes its pixels until the shell presents it");
543 
544         // The last frame was never presented: this one repaints everything.
545         let f = build_frame(&mut app, &mut fs, SIZE, 2.0, &mut owed, &mut items);
546         assert_eq!(f.damage, None);
547     }
548 
549     #[test]
550     fn custom_vertices_are_a_last_unclipped_batch_and_overlays_their_own() {
551         let mut app = Mock { custom: 2, overlay: true, ..Mock::default() };
552         let (mut fs, mut items, mut owed) = (fonts(), Vec::new(), false);
553         let f = build_frame(&mut app, &mut fs, SIZE, 1.0, &mut owed, &mut items);
554         // An empty display list: the custom quads are the whole vertex list,
555         // in one batch on top with no scissor.
556         assert_eq!(f.verts.len(), 12);
557         let last = f.batches.last().expect("a batch for the custom vertices");
558         assert_eq!((last.start, last.end, last.scissor), (0, 12, None));
559         assert_eq!(f.overlay_verts.len(), 6);
560 
561         let none = build_frame(&mut Mock::default(), &mut fs, SIZE, 1.0, &mut owed, &mut items);
562         assert!(none.verts.is_empty() && none.batches.is_empty());
563     }
564 
565     #[test]
566     fn the_clear_colour_is_linearized_and_its_alpha_kept() {
567         let (mut fs, mut items, mut owed) = (fonts(), Vec::new(), false);
568         let f = build_frame(&mut Mock::default(), &mut fs, SIZE, 1.0, &mut owed, &mut items);
569         let [r, g, b, a] = f.clear_color;
570         assert!((r - 0.5f32.powf(2.2)).abs() < 1e-6 && g == 0.0 && b == 1.0 && a == 0.25);
571         // And the frame lends the renderer exactly what it built.
572         let f2 = f.frame2d();
573         assert_eq!((f2.clear_color, f2.damage), (f.clear_color, f.damage));
574     }
575 }
576 
577 #[cfg(test)]
578 mod damage_tests {
579     use super::*;
580 
581     fn k(v: &[(u64, i32)]) -> Vec<(u64, Option<PxBox>)> {
582         v.iter().map(|&(key, x)| (key, Some((x, 0, x + 10, 10)))).collect()
583     }
584 
585     /// Unchanged runs are no damage; a changed, inserted or removed item is
586     /// damaged where it was and where it is, and nothing beyond the common
587     /// prefix and suffix is.
588     #[test]
589     fn diff_runs_takes_the_middle() {
590         let (a, b) = (k(&[(1, 0), (2, 20), (3, 40)]), k(&[(1, 0), (2, 20), (3, 40)]));
591         assert_eq!(diff_runs(&a, &b, |x| x.0, |x| x.1), None);
592         let changed = k(&[(1, 0), (9, 25), (3, 40)]);
593         assert_eq!(diff_runs(&a, &changed, |x| x.0, |x| x.1), Some((20, 0, 35, 10)));
594         let inserted = k(&[(1, 0), (7, 60), (2, 20), (3, 40)]);
595         assert_eq!(diff_runs(&a, &inserted, |x| x.0, |x| x.1), Some((60, 0, 70, 10)));
596         let removed = k(&[(1, 0), (3, 40)]);
597         assert_eq!(diff_runs(&a, &removed, |x| x.0, |x| x.1), Some((20, 0, 30, 10)));
598     }
599 
600     /// An item with no box (nothing to bound it by) damages everything.
601     #[test]
602     fn an_unbounded_change_is_everything() {
603         let a = vec![(1u64, None)];
604         let b = vec![(2u64, None)];
605         let d = diff_runs(&a, &b, |x: &(u64, Option<PxBox>)| x.0, |x| x.1).unwrap();
606         assert!(d.0 < -1_000_000 && d.2 > 1_000_000);
607     }
608 
609     /// The same string in another face is a changed text item: a font
610     /// picker's preview keeps its text and swaps only the face, and was left
611     /// showing the old one.
612     #[test]
613     fn the_same_text_in_another_face_is_damage() {
614         use crate::scene::paint::TextAttrs;
615         let frame = BuiltFrame {
616             verts: Vec::new(),
617             batches: Vec::new(),
618             overlay_verts: Vec::new(),
619             images: Vec::new(),
620             plate_features: Vec::new(),
621             clear_color: [0.0; 4],
622             damage: None,
623             dl_text: true,
624             overlay_rects: Vec::new(),
625             scale: 1.0,
626             physical: (400, 200),
627         };
628         // A family the font system holds in both an upright and an italic face, so the two
629         // really are different pictures. Asked for a generic family that resolves to nothing
630         // installed (a macOS runner's "monospace"), the per-glyph fallback can draw both
631         // styles with one upright face and no fake-italic flag: the same picture, and no
632         // damage is then the right answer, not a failure.
633         let family = {
634             use cosmic_text::fontdb::Style;
635             let fs = crate::geometry_font_system().lock().unwrap();
636             let mut styles: std::collections::HashMap<String, (bool, bool)> = Default::default();
637             for f in fs.db().faces() {
638                 if let Some((name, _)) = f.families.first() {
639                     let e = styles.entry(name.clone()).or_default();
640                     match f.style {
641                         Style::Normal => e.0 = true,
642                         Style::Italic => e.1 = true,
643                         Style::Oblique => {}
644                     }
645                 }
646             }
647             let mut both: Vec<String> = styles.into_iter().filter(|(_, (n, i))| *n && *i).map(|(k, _)| k).collect();
648             both.sort();
649             both.into_iter().next()
650         };
651         let Some(family) = family else {
652             eprintln!("skipping: no font family here has both an upright and an italic face");
653             return;
654         };
655         let key = |attrs: TextAttrs| {
656             let mut fs = crate::geometry_font_system().lock().unwrap();
657             let buffer = crate::backend::text::shared_text_buffer(&mut fs, "The quick brown fox", 14.0, Some(&family), attrs);
658             let t = DlText { buffer, x: 10.0, y: 10.0, color: cosmic_text::Color::rgb(255, 255, 255), bounds: None, clip_circle: None, clip_rrect: None };
659             FrameSig::of(&frame, &[t]).texts[0].0
660         };
661         let upright = TextAttrs::default();
662         assert_eq!(key(upright), key(upright), "an unchanged item is no damage");
663         assert_ne!(key(upright), key(TextAttrs { italic: true, ..upright }), "{family}: italic is another face");
664     }
665 }