Skip to main content

sequoia_view/
damage.rs

1//! Model inwalidacji między klatkami.
2//!
3//! `DamageTracker` akumuluje marki z jednej klatki, żeby runtime mógł:
4//! - **pominąć całą klatkę**, gdy nie ma pracy malowania ani animacji,
5//! - **ograniczyć repaint** do bounding-boxu obrażeń (Phase 2 — scissor),
6//! - **nie pominąć klatki**, gdy trwa animacja (`request_frame`).
7//!
8//! # Co triggeruje klatkę
9//!
10//! - `Runtime::request_rebuild` (sygnały, effecty, stan widoku, mount/unmount)
11//!   → `Dirty::LAYOUT | Dirty::CHILDREN`: rebuild w `settle`, potem pełny paint.
12//! - `Runtime::mark_damage(.., Dirty::PAINT)` (hover/press/focus)
13//!   → `PAINT` + bounding-box elementu: paint bez rebuild.
14//! - `PaintCtx::request_frame` (animacja) → motion: paint następnej klatki.
15//! - `Runtime::mark_full_damage` (resize, DPI, motyw, nowy root)
16//!   → pełny viewport i unieważnienie cache warstw.
17//! - `Runtime::layout` z innym viewportem → pełny paint + `LAYOUT`.
18//!
19//! `HIT_TEST` i `DATA` są tylko znacznikami księgowymi: nie wymagają pikseli
20//! i same nie tworzą klatki.
21//!
22//! # Fazy klatki
23//!
24//! 1. `tick()` → `settle()` scala dirty per element i przebudowuje tylko
25//!    elementy z `needs_layout()`. Czysto-paintowe marki nie robią rebuild.
26//! 2. `layout()` rozwiązuje geometrię; zmiana viewportu wymusza pełny paint.
27//! 3. `paint_if_dirty()`:
28//!    - bierze `take_motion()` (zużywa request z poprzedniej klatki),
29//!    - jeśli brak motion i brak `needs_repaint()` → `record_skipped`,
30//!      return `Skipped`,
31//!    - inaczej: paint (partial bbox lub full), `after_paint()`, return `Painted`.
32//!
33//! # Agregacja
34//!
35//! - wielokrotne marki tego samego elementu: OR flag, element trafia do queue raz,
36//! - wiele elementów: jeden `settle`, jeden paint i jeden bbox (union) na klatkę,
37//! - marki spoza viewportu i tak trafiają do bboxu, dopóki nie ma `full`/layoutu.
38
39use sequoia_core::{Dirty, Rect};
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub enum FrameOutcome {
43    Painted,
44    Skipped,
45}
46
47/// Upper bound on disjoint damage regions kept for one frame.
48///
49/// Extra marks collapse into a single bounding box so the renderer never
50/// records an unbounded number of scissored passes.
51pub(crate) const MAX_DAMAGE_REGIONS: usize = 8;
52
53#[derive(Clone, Debug)]
54pub struct DamageTracker {
55    // �?" stan bie���cej klatki �?" -
56    flags: Dirty,
57    regions: Vec<Rect>,
58    full: bool,
59
60    // — "motion" (request_frame z poprzedniej klatki) —
61    motion_pending: bool,
62
63    // — statystyki —
64    frames_painted: u64,
65    frames_skipped: u64,
66}
67
68impl Default for DamageTracker {
69    fn default() -> Self {
70        Self {
71            flags: Dirty::empty(),
72            regions: Vec::new(),
73            full: false,
74            motion_pending: false,
75            frames_painted: 0,
76            frames_skipped: 0,
77        }
78    }
79}
80
81impl DamageTracker {
82    pub fn new() -> Self {
83        Self::default()
84    }
85
86    // ──────────────────────────────────────────────────────────
87    // Markery (wołane w trakcie klatki, przed paint)
88    // ──────────────────────────────────────────────────────────
89
90    /// Zaznacz konkretny obszar (window coords) jako brudny.
91    pub fn mark_rect(&mut self, rect: Rect) {
92        if rect.is_empty() {
93            return;
94        }
95        self.flags |= Dirty::PAINT;
96        if !self.full {
97            self.add_region(rect);
98        }
99    }
100
101    fn add_region(&mut self, rect: Rect) {
102        let mut merged = rect;
103        let mut i = 0;
104        while i < self.regions.len() {
105            if self.regions[i].intersects(merged) {
106                merged = merged.union(self.regions.remove(i));
107            } else {
108                i += 1;
109            }
110        }
111        self.regions.push(merged);
112        if self.regions.len() > MAX_DAMAGE_REGIONS {
113            let all = self.regions.drain(..).fold(Rect::ZERO, Rect::union);
114            self.regions.push(all);
115        }
116    }
117
118    /// Cały viewport do przemalowania (resize, DPI, motyw, mount/unmount root).
119    pub fn mark_full(&mut self) {
120        self.full = true;
121        self.flags |= Dirty::PAINT | Dirty::SURFACE;
122    }
123
124    /// Zmiana wpłynie na pomiar/układ (nie tylko piksele).
125    pub fn mark_layout(&mut self) {
126        self.flags |= Dirty::LAYOUT;
127    }
128
129    /// Surowy mark (np. `Dirty::OVERLAY`, `Dirty::CANVAS`).
130    pub fn mark(&mut self, flags: Dirty) {
131        self.flags |= flags;
132    }
133
134    // ──────────────────────────────────────────────────────────
135    // Motion (animacje, spinner, scroll velocity)
136    // ──────────────────────────────────────────────────────────
137
138    /// Wołane przez `PaintCtx::request_frame()` — "pomaluj mnie w następnej klatce".
139    pub fn request_frame(&mut self) {
140        self.motion_pending = true;
141    }
142
143    /// Zużywa request z poprzedniej klatki. Wołane raz na klatkę, PRZED paint.
144    pub fn take_motion(&mut self) -> bool {
145        let was = self.motion_pending;
146        self.motion_pending = false;
147        was
148    }
149
150    // ──────────────────────────────────────────────────────────
151    // Odpytania
152    // ──────────────────────────────────────────────────────────
153
154    /// No pixels or animation work are pending.
155    ///
156    /// Bookkeeping-only flags (`HIT_TEST`, `DATA`) do not count as a frame.
157    pub fn is_empty(&self) -> bool {
158        !self.motion_pending && !self.flags.needs_repaint()
159    }
160
161    pub fn needs_frame(&self) -> bool {
162        !self.is_empty()
163    }
164
165    pub fn needs_layout(&self) -> bool {
166        self.flags.needs_layout()
167    }
168
169    pub fn needs_paint(&self) -> bool {
170        self.flags.needs_repaint()
171    }
172
173    pub fn flags(&self) -> Dirty {
174        self.flags
175    }
176
177    /// `Some(rect)` = repaint only this area. `None` = full repaint.
178    ///
179    /// Layout changes can move or remove children, so they always require a
180    /// full repaint; a screen-space rect alone cannot describe that damage.
181    /// The 2 px margin covers anti-aliasing; the renderer adds more headroom
182    /// for shadows before using the region as a scissor.
183    ///
184    /// This is the bounding box of [`paint_regions`](Self::paint_regions); use
185    /// the individual regions when the backend supports multiple scissors.
186    pub fn paint_region(&self) -> Option<Rect> {
187        self.paint_regions()
188            .map(|regions| regions.into_iter().fold(Rect::ZERO, Rect::union))
189    }
190
191    /// `Some(regions)` = repaint only these disjoint areas. `None` = full
192    /// repaint.
193    ///
194    /// Each region is inflated by 2 px for anti-aliasing. Regions that overlap
195    /// are merged while marks accumulate; if more than the region cap
196    /// remain, they collapse into one bounding box.
197    pub fn paint_regions(&self) -> Option<Vec<Rect>> {
198        if self.full || self.flags.needs_layout() || self.regions.is_empty() {
199            None
200        } else {
201            Some(
202                self.regions
203                    .iter()
204                    .map(|rect| rect.inflate(2.0, 2.0))
205                    .collect(),
206            )
207        }
208    }
209
210    // ──────────────────────────────────────────────────────────
211    // Koniec klatki
212    // ──────────────────────────────────────────────────────────
213
214    /// Wołane po paint — resetuje stan bieżącej klatki.
215    /// **NIE** rusza `motion_pending`, bo paint mógł złożyć nowy request.
216    pub fn after_paint(&mut self) {
217        self.flags = Dirty::empty();
218        self.regions.clear();
219        self.full = false;
220        self.frames_painted += 1;
221    }
222
223    /// Wołane gdy klatka pominięta.
224    ///
225    /// Czyści też znaczniki księgowe (`HIT_TEST`, `DATA`), które nie wymagają
226    /// pikseli, żeby nie utrzymywały `needs_frame()` w nieskończoność.
227    pub fn record_skipped(&mut self) {
228        self.flags = Dirty::empty();
229        self.regions.clear();
230        self.full = false;
231        self.frames_skipped += 1;
232    }
233
234    /// Pełny reset (nowy root, nowe okno).
235    pub fn clear_all(&mut self) {
236        self.flags = Dirty::empty();
237        self.regions.clear();
238        self.full = false;
239        self.motion_pending = false;
240    }
241
242    // ──────────────────────────────────────────────────────────
243    // Statystyki
244    // ──────────────────────────────────────────────────────────
245
246    pub fn frames_painted(&self) -> u64 {
247        self.frames_painted
248    }
249
250    pub fn frames_skipped(&self) -> u64 {
251        self.frames_skipped
252    }
253
254    /// Odsetek klatek pominiętych (0.0 – 1.0).
255    pub fn skip_rate(&self) -> f32 {
256        let total = self.frames_painted + self.frames_skipped;
257        if total == 0 {
258            0.0
259        } else {
260            self.frames_skipped as f32 / total as f32
261        }
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268    use sequoia_core::Point;
269
270    fn r(x: f32, y: f32, w: f32, h: f32) -> Rect {
271        Rect::from_xywh(x, y, w, h)
272    }
273
274    #[test]
275    fn fresh_tracker_is_empty() {
276        let d = DamageTracker::new();
277        assert!(d.is_empty());
278        assert!(!d.needs_frame());
279        assert!(!d.needs_layout());
280        assert!(!d.needs_paint());
281        assert_eq!(d.paint_region(), None);
282    }
283
284    #[test]
285    fn single_rect_marks_paint_only() {
286        let mut d = DamageTracker::new();
287        d.mark_rect(r(10.0, 10.0, 4.0, 4.0));
288        assert!(!d.is_empty());
289        assert!(!d.needs_layout(), "sam mark_rect nie zmienia layoutu");
290        assert!(d.needs_paint());
291        let region = d.paint_region().unwrap();
292        assert!(region.contains(Point::new(12.0, 12.0)));
293        assert!(region.size.width >= 4.0);
294    }
295
296    #[test]
297    fn disjoint_rects_stay_separate_regions() {
298        let mut d = DamageTracker::new();
299        d.mark_rect(r(0.0, 0.0, 10.0, 10.0));
300        d.mark_rect(r(100.0, 50.0, 4.0, 4.0));
301        let regions = d.paint_regions().unwrap();
302        assert_eq!(regions.len(), 2);
303        assert!(regions[0].right() < regions[1].left());
304        assert!(regions[0].bottom() < regions[1].top());
305
306        let bbox = d.paint_region().unwrap();
307        assert!(bbox.left() <= 0.0);
308        assert!(bbox.right() >= 104.0);
309        assert!(bbox.top() <= 0.0);
310        assert!(bbox.bottom() >= 54.0);
311    }
312
313    #[test]
314    fn overlapping_rects_merge_into_one_region() {
315        let mut d = DamageTracker::new();
316        d.mark_rect(r(0.0, 0.0, 10.0, 10.0));
317        d.mark_rect(r(5.0, 5.0, 10.0, 10.0));
318        let regions = d.paint_regions().unwrap();
319        assert_eq!(regions.len(), 1);
320        assert!(regions[0].contains(Point::new(12.0, 12.0)));
321    }
322
323    #[test]
324    fn too_many_regions_collapse_to_bbox() {
325        let mut d = DamageTracker::new();
326        for i in 0..=MAX_DAMAGE_REGIONS {
327            d.mark_rect(r(i as f32 * 10.0, 0.0, 1.0, 1.0));
328        }
329        let regions = d.paint_regions().unwrap();
330        assert_eq!(regions.len(), 1, "bounded number of scissor passes");
331        assert!(regions[0].right() >= MAX_DAMAGE_REGIONS as f32 * 10.0);
332    }
333
334    #[test]
335    fn empty_rects_are_ignored() {
336        let mut d = DamageTracker::new();
337        d.mark_rect(Rect::ZERO);
338        d.mark_rect(r(5.0, 5.0, 0.0, 0.0));
339        assert!(d.is_empty(), "puste recty nie liczą się");
340    }
341
342    #[test]
343    fn full_wins_over_rects() {
344        let mut d = DamageTracker::new();
345        d.mark_rect(r(0.0, 0.0, 10.0, 10.0));
346        d.mark_full();
347        assert_eq!(d.paint_region(), None, "full = brak partial clip");
348        assert!(d.flags().contains(Dirty::SURFACE));
349    }
350
351    #[test]
352    fn layout_flag_propagates() {
353        let mut d = DamageTracker::new();
354        d.mark_layout();
355        assert!(d.needs_layout());
356        assert!(d.needs_paint(), "layout implies paint");
357        assert_eq!(d.paint_region(), None, "no rect = full paint");
358    }
359
360    #[test]
361    fn layout_with_rect_still_requires_full_paint() {
362        let mut d = DamageTracker::new();
363        d.mark_rect(r(10.0, 10.0, 20.0, 20.0));
364        d.mark_layout();
365        assert_eq!(
366            d.paint_region(),
367            None,
368            "layout changes can move or remove children"
369        );
370    }
371
372    #[test]
373    fn request_frame_forces_next_paint() {
374        let mut d = DamageTracker::new();
375        assert!(!d.needs_frame());
376        d.request_frame();
377        assert!(d.needs_frame(), "motion = ramka do namalowania");
378
379        // take_motion zużywa request
380        assert!(d.take_motion());
381        assert!(!d.take_motion(), "drugie take_motion = false");
382    }
383
384    #[test]
385    fn bookkeeping_flags_do_not_request_a_frame() {
386        let mut d = DamageTracker::new();
387        d.mark(Dirty::HIT_TEST | Dirty::DATA);
388        assert!(!d.needs_paint());
389        assert!(!d.needs_frame(), "hit-test/data alone do not paint pixels");
390
391        d.record_skipped();
392        assert_eq!(d.flags(), Dirty::empty());
393        assert!(!d.needs_frame());
394    }
395
396    #[test]
397    fn paint_without_rect_requests_full_repaint() {
398        let mut d = DamageTracker::new();
399        d.mark(Dirty::PAINT);
400        assert!(d.needs_frame());
401        assert_eq!(
402            d.paint_region(),
403            None,
404            "unknown pixel bounds require a full repaint"
405        );
406    }
407
408    #[test]
409    fn after_paint_clears_rects_but_not_motion() {
410        let mut d = DamageTracker::new();
411        d.mark_rect(r(0.0, 0.0, 10.0, 10.0));
412        d.after_paint();
413        assert!(d.is_empty(), "po klatce puste");
414
415        // paint mógł ustawić motion dla następnej klatki
416        d.request_frame();
417        d.after_paint();
418        assert!(d.needs_frame(), "motion z paint nie ginie");
419    }
420
421    #[test]
422    fn clear_all_resets_everything() {
423        let mut d = DamageTracker::new();
424        d.mark_rect(r(0.0, 0.0, 10.0, 10.0));
425        d.mark_layout();
426        d.request_frame();
427        d.clear_all();
428        assert!(d.is_empty());
429        assert!(!d.needs_frame());
430        assert_eq!(d.paint_region(), None);
431    }
432
433    #[test]
434    fn stats_track_painted_and_skipped() {
435        let mut d = DamageTracker::new();
436        d.mark_rect(r(0.0, 0.0, 1.0, 1.0));
437        d.after_paint();
438        d.record_skipped();
439        d.record_skipped();
440        d.mark_rect(r(0.0, 0.0, 1.0, 1.0));
441        d.after_paint();
442        assert_eq!(d.frames_painted(), 2);
443        assert_eq!(d.frames_skipped(), 2);
444        assert!((d.skip_rate() - 0.5).abs() < 1e-6);
445    }
446
447    #[test]
448    fn many_marks_coalesce_into_one_frame() {
449        let mut d = DamageTracker::new();
450        for i in 0..100 {
451            d.mark_rect(r(i as f32, 0.0, 1.0, 1.0));
452        }
453        // 100 marków = jedna klatka
454        d.after_paint();
455        assert_eq!(d.frames_painted(), 1);
456        assert!(d.is_empty());
457    }
458}