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 }