i_slint_core/
partial_renderer.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4//! Module for a renderer proxy that tries to render only the parts of the tree that have changed.
5//!
6//! This is the way the partial renderer work:
7//!
8//! 1. [`PartialRenderer::compute_dirty_regions`] will go over the items and try to compute the region that needs to be repainted.
9//!    If either the bounding box has changed, or the PropertyTracker that tracks the rendering properties is dirty, then the
10//!    region is marked dirty.
11//!    That pass also register dependencies on every geometry, and on the non-dirty property trackers.
12//! 2. The Renderer calls [`PartialRenderer::filter_item`] For most items.
13//!    This assume that the cached geometry was requested in the previous step. So it will not register new dependencies.
14//! 3. Then the renderer calls the rendering function for each item that needs to be rendered.
15//!    This register dependencies only on the rendering tracker.
16//!
17
18use crate::item_rendering::{
19    ItemRenderer, ItemRendererFeatures, RenderBorderRectangle, RenderImage, RenderRectangle,
20    RenderText,
21};
22use crate::item_tree::{ItemTreeRc, ItemTreeWeak, ItemVisitorResult};
23#[cfg(feature = "std")]
24use crate::items::Path;
25use crate::items::{BoxShadow, Clip, ItemRc, ItemRef, Opacity, RenderingResult, TextInput};
26use crate::lengths::{
27    ItemTransform, LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalPx, LogicalRect,
28    LogicalSize, LogicalVector,
29};
30use crate::properties::PropertyTracker;
31use crate::window::WindowAdapter;
32use crate::Coord;
33use alloc::boxed::Box;
34use alloc::rc::Rc;
35use core::cell::{Cell, RefCell};
36use core::pin::Pin;
37
38/// This structure must be present in items that are Rendered and contains information.
39/// Used by the backend.
40#[derive(Default, Debug)]
41#[repr(C)]
42pub struct CachedRenderingData {
43    /// Used and modified by the backend, should be initialized to 0 by the user code
44    pub(crate) cache_index: Cell<usize>,
45    /// Used and modified by the backend, should be initialized to 0 by the user code.
46    /// The backend compares this generation against the one of the cache to verify
47    /// the validity of the cache_index field.
48    pub(crate) cache_generation: Cell<usize>,
49}
50
51impl CachedRenderingData {
52    /// This function can be used to remove an entry from the rendering cache for a given item, if it
53    /// exists, i.e. if any data was ever cached. This is typically called by the graphics backend's
54    /// implementation of the release_item_graphics_cache function.
55    fn release(
56        &self,
57        cache: &mut PartialRendererCache,
58    ) -> Option<CachedItemBoundingBoxAndTransform> {
59        if self.cache_generation.get() == cache.generation() {
60            let index = self.cache_index.get();
61            self.cache_generation.set(0);
62            Some(cache.remove(index).data)
63        } else {
64            None
65        }
66    }
67
68    /// Return the value if it is in the cache
69    fn get_entry<'a>(
70        &self,
71        cache: &'a mut PartialRendererCache,
72    ) -> Option<&'a mut PartialRenderingCachedData> {
73        let index = self.cache_index.get();
74        if self.cache_generation.get() == cache.generation() {
75            cache.get_mut(index)
76        } else {
77            None
78        }
79    }
80}
81
82/// After rendering an item, we cache the geometry and the transform it applies to
83/// children.
84#[derive(Clone, PartialEq)]
85pub enum CachedItemBoundingBoxAndTransform {
86    /// A regular item with a translation
87    RegularItem {
88        /// The item's bounding rect relative to its parent.
89        bounding_rect: LogicalRect,
90        /// The item's offset relative to its parent.
91        offset: LogicalVector,
92    },
93    /// An item such as Rotate that defines an additional transformation
94    ItemWithTransform {
95        /// The item's bounding rect relative to its parent.
96        bounding_rect: LogicalRect,
97        /// The item's transform to apply to children.
98        transform: Box<ItemTransform>,
99    },
100    /// A clip item.
101    ClipItem {
102        /// The item's geometry relative to its parent.
103        geometry: LogicalRect,
104    },
105}
106
107impl CachedItemBoundingBoxAndTransform {
108    fn bounding_rect(&self) -> &LogicalRect {
109        match self {
110            CachedItemBoundingBoxAndTransform::RegularItem { bounding_rect, .. } => bounding_rect,
111            CachedItemBoundingBoxAndTransform::ItemWithTransform { bounding_rect, .. } => {
112                bounding_rect
113            }
114            CachedItemBoundingBoxAndTransform::ClipItem { geometry } => geometry,
115        }
116    }
117
118    fn transform(&self) -> ItemTransform {
119        match self {
120            CachedItemBoundingBoxAndTransform::RegularItem { offset, .. } => {
121                ItemTransform::translation(offset.x as f32, offset.y as f32)
122            }
123            CachedItemBoundingBoxAndTransform::ItemWithTransform { transform, .. } => **transform,
124            CachedItemBoundingBoxAndTransform::ClipItem { geometry } => {
125                ItemTransform::translation(geometry.origin.x as f32, geometry.origin.y as f32)
126            }
127        }
128    }
129
130    fn new<T: ItemRendererFeatures>(
131        item_rc: &ItemRc,
132        window_adapter: &Rc<dyn WindowAdapter>,
133    ) -> Self {
134        let geometry = item_rc.geometry();
135
136        if item_rc.borrow().as_ref().clips_children() {
137            return Self::ClipItem { geometry };
138        }
139
140        // Evaluate the bounding rect untracked, as properties that affect the bounding rect are already tracked
141        // at rendering time.
142        let bounding_rect = crate::properties::evaluate_no_tracking(|| {
143            item_rc.bounding_rect(&geometry, window_adapter)
144        });
145
146        if let Some(complex_child_transform) = (T::SUPPORTS_TRANSFORMATIONS
147            && window_adapter.renderer().supports_transformations())
148        .then(|| item_rc.children_transform())
149        .flatten()
150        {
151            Self::ItemWithTransform {
152                bounding_rect,
153                transform: complex_child_transform
154                    .then_translate(geometry.origin.to_vector().cast())
155                    .into(),
156            }
157        } else {
158            Self::RegularItem { bounding_rect, offset: geometry.origin.to_vector() }
159        }
160    }
161}
162
163struct PartialRenderingCachedData {
164    /// The geometry of the item as it was previously rendered.
165    pub data: CachedItemBoundingBoxAndTransform,
166    /// The property tracker that should be used to evaluate whether the item needs to be re-rendered
167    pub tracker: Option<core::pin::Pin<Box<PropertyTracker>>>,
168}
169impl PartialRenderingCachedData {
170    fn new(data: CachedItemBoundingBoxAndTransform) -> Self {
171        Self { data, tracker: None }
172    }
173}
174
175/// The cache that needs to be held by the Window for the partial rendering
176struct PartialRendererCache {
177    slab: slab::Slab<PartialRenderingCachedData>,
178    generation: usize,
179}
180
181impl Default for PartialRendererCache {
182    fn default() -> Self {
183        Self { slab: Default::default(), generation: 1 }
184    }
185}
186
187impl PartialRendererCache {
188    /// Returns the generation of the cache. The generation starts at 1 and is increased
189    /// whenever the cache is cleared, for example when the GL context is lost.
190    pub fn generation(&self) -> usize {
191        self.generation
192    }
193
194    /// Retrieves a mutable reference to the cached graphics data at index.
195    pub fn get_mut(&mut self, index: usize) -> Option<&mut PartialRenderingCachedData> {
196        self.slab.get_mut(index)
197    }
198
199    /// Inserts data into the cache and returns the index for retrieval later.
200    pub fn insert(&mut self, data: PartialRenderingCachedData) -> usize {
201        self.slab.insert(data)
202    }
203
204    /// Removes the cached graphics data at the given index.
205    pub fn remove(&mut self, index: usize) -> PartialRenderingCachedData {
206        self.slab.remove(index)
207    }
208
209    /// Removes all entries from the cache and increases the cache's generation count, so
210    /// that stale index access can be avoided.
211    pub fn clear(&mut self) {
212        self.slab.clear();
213        self.generation += 1;
214    }
215}
216
217/// A region composed of a few rectangles that need to be redrawn.
218#[derive(Default, Clone)]
219pub struct DirtyRegion {
220    rectangles: [euclid::Box2D<Coord, LogicalPx>; Self::MAX_COUNT],
221    count: usize,
222}
223
224impl core::fmt::Debug for DirtyRegion {
225    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
226        write!(f, "{:?}", &self.rectangles[..self.count])
227    }
228}
229
230impl DirtyRegion {
231    /// The maximum number of rectangles that can be stored in a DirtyRegion
232    pub(crate) const MAX_COUNT: usize = 3;
233
234    /// An iterator over the part of the region (they can overlap)
235    pub fn iter(&self) -> impl Iterator<Item = euclid::Box2D<Coord, LogicalPx>> + '_ {
236        (0..self.count).map(|x| self.rectangles[x])
237    }
238
239    /// Add a rectangle to the region.
240    ///
241    /// Note that if the region becomes too complex, it might be simplified by being bigger than the actual union.
242    pub fn add_rect(&mut self, rect: LogicalRect) {
243        self.add_box(rect.to_box2d());
244    }
245
246    /// Add a box to the region
247    ///
248    /// Note that if the region becomes too complex, it might be simplified by being bigger than the actual union.
249    pub fn add_box(&mut self, b: euclid::Box2D<Coord, LogicalPx>) {
250        if b.is_empty() {
251            return;
252        }
253        let mut i = 0;
254        while i < self.count {
255            let r = &self.rectangles[i];
256            if r.contains_box(&b) {
257                // the rectangle is already in the union
258                return;
259            } else if b.contains_box(r) {
260                self.rectangles.swap(i, self.count - 1);
261                self.count -= 1;
262                continue;
263            }
264            i += 1;
265        }
266
267        if self.count < Self::MAX_COUNT {
268            self.rectangles[self.count] = b;
269            self.count += 1;
270        } else {
271            let best_merge = (0..self.count)
272                .map(|i| (i, self.rectangles[i].union(&b).area() - self.rectangles[i].area()))
273                .min_by(|a, b| PartialOrd::partial_cmp(&a.1, &b.1).unwrap())
274                .expect("There should always be rectangles")
275                .0;
276            self.rectangles[best_merge] = self.rectangles[best_merge].union(&b);
277        }
278    }
279
280    /// Make an union of two regions.
281    ///
282    /// Note that if the region becomes too complex, it might be simplified by being bigger than the actual union
283    #[must_use]
284    pub fn union(&self, other: &Self) -> Self {
285        let mut s = self.clone();
286        for o in other.iter() {
287            s.add_box(o)
288        }
289        s
290    }
291
292    /// Bounding rectangle of the region.
293    #[must_use]
294    pub fn bounding_rect(&self) -> LogicalRect {
295        if self.count == 0 {
296            return Default::default();
297        }
298        let mut r = self.rectangles[0];
299        for i in 1..self.count {
300            r = r.union(&self.rectangles[i]);
301        }
302        r.to_rect()
303    }
304
305    /// Intersection of a region and a rectangle.
306    #[must_use]
307    pub fn intersection(&self, other: LogicalRect) -> DirtyRegion {
308        let mut ret = self.clone();
309        let other = other.to_box2d();
310        let mut i = 0;
311        while i < ret.count {
312            if let Some(x) = ret.rectangles[i].intersection(&other) {
313                ret.rectangles[i] = x;
314            } else {
315                ret.count -= 1;
316                ret.rectangles.swap(i, ret.count);
317                continue;
318            }
319            i += 1;
320        }
321        ret
322    }
323
324    fn draw_intersects(&self, clipped_geom: LogicalRect) -> bool {
325        let b = clipped_geom.to_box2d();
326        self.iter().any(|r| r.intersects(&b))
327    }
328}
329
330impl From<LogicalRect> for DirtyRegion {
331    fn from(value: LogicalRect) -> Self {
332        let mut s = Self::default();
333        s.add_rect(value);
334        s
335    }
336}
337
338/// This enum describes which parts of the buffer passed to the [`SoftwareRenderer`](crate::software_renderer::SoftwareRenderer) may be re-used to speed up painting.
339// FIXME: #[non_exhaustive] #3023
340#[derive(PartialEq, Eq, Debug, Clone, Default, Copy)]
341pub enum RepaintBufferType {
342    #[default]
343    /// The full window is always redrawn. No attempt at partial rendering will be made.
344    NewBuffer,
345    /// Only redraw the parts that have changed since the previous call to render().
346    ///
347    /// This variant assumes that the same buffer is passed on every call to render() and
348    /// that it still contains the previously rendered frame.
349    ReusedBuffer,
350
351    /// Redraw the part that have changed since the last two frames were drawn.
352    ///
353    /// This is used when using double buffering and swapping of the buffers.
354    SwappedBuffers,
355}
356
357/// Put this structure in the renderer to help with partial rendering
358///
359/// This is constructed from a [`PartialRenderingState`]
360pub struct PartialRenderer<'a, T> {
361    cache: &'a RefCell<PartialRendererCache>,
362    /// The region of the screen which is considered dirty and that should be repainted
363    pub dirty_region: DirtyRegion,
364    /// The actual renderer which the drawing call will be forwarded to
365    pub actual_renderer: T,
366    /// The window adapter the renderer is rendering into.
367    pub window_adapter: Rc<dyn WindowAdapter>,
368}
369
370impl<'a, T: ItemRenderer + ItemRendererFeatures> PartialRenderer<'a, T> {
371    /// Create a new PartialRenderer
372    fn new(
373        cache: &'a RefCell<PartialRendererCache>,
374        initial_dirty_region: DirtyRegion,
375        actual_renderer: T,
376    ) -> Self {
377        let window_adapter = actual_renderer.window().window_adapter();
378        Self { cache, dirty_region: initial_dirty_region, actual_renderer, window_adapter }
379    }
380
381    /// Visit the tree of item and compute what are the dirty regions
382    pub fn compute_dirty_regions(
383        &mut self,
384        component: &ItemTreeRc,
385        origin: LogicalPoint,
386        size: LogicalSize,
387    ) {
388        #[derive(Clone, Copy)]
389        struct ComputeDirtyRegionState {
390            transform_to_screen: ItemTransform,
391            old_transform_to_screen: ItemTransform,
392            clipped: LogicalRect,
393            must_refresh_children: bool,
394        }
395
396        impl ComputeDirtyRegionState {
397            /// Adjust transform_to_screen and old_transform_to_screen to map from item coordinates
398            /// to the screen when using it on a child, specified by its children transform.
399            fn adjust_transforms_for_child(
400                &mut self,
401                children_transform: &ItemTransform,
402                old_children_transform: &ItemTransform,
403            ) {
404                self.transform_to_screen = children_transform.then(&self.transform_to_screen);
405                self.old_transform_to_screen =
406                    old_children_transform.then(&self.old_transform_to_screen);
407            }
408        }
409
410        crate::item_tree::visit_items(
411            component,
412            crate::item_tree::TraversalOrder::BackToFront,
413            |component, item, index, state| {
414                let mut new_state = *state;
415                let item_rc = ItemRc::new(component.clone(), index);
416                let rendering_data = item.cached_rendering_data_offset();
417                let mut cache = self.cache.borrow_mut();
418
419                match rendering_data.get_entry(&mut cache) {
420                    Some(PartialRenderingCachedData { data: cached_geom, tracker }) => {
421                        let rendering_dirty = tracker.as_ref().is_some_and(|tr| tr.is_dirty());
422                        let old_geom = cached_geom.clone();
423                        let new_geom = CachedItemBoundingBoxAndTransform::new::<T>(
424                            &item_rc,
425                            &self.window_adapter,
426                        );
427
428                        let geometry_changed = old_geom != new_geom;
429                        if ItemRef::downcast_pin::<Clip>(item).is_some()
430                            || ItemRef::downcast_pin::<Opacity>(item).is_some()
431                        {
432                            // When the opacity or the clip change, this will impact all the children, including
433                            // the ones outside the element, regardless if they are themselves dirty or not.
434                            new_state.must_refresh_children |= rendering_dirty || geometry_changed;
435
436                            if rendering_dirty {
437                                // Destroy the tracker as we we might not re-render this clipped item but it would stay dirty
438                                *tracker = None;
439                            }
440                        }
441
442                        if geometry_changed {
443                            self.mark_dirty_rect(
444                                old_geom.bounding_rect(),
445                                state.old_transform_to_screen,
446                                &state.clipped,
447                            );
448                            self.mark_dirty_rect(
449                                new_geom.bounding_rect(),
450                                state.transform_to_screen,
451                                &state.clipped,
452                            );
453
454                            new_state.adjust_transforms_for_child(
455                                &new_geom.transform(),
456                                &old_geom.transform(),
457                            );
458
459                            *cached_geom = new_geom;
460
461                            return ItemVisitorResult::Continue(new_state);
462                        }
463
464                        new_state.adjust_transforms_for_child(
465                            &cached_geom.transform(),
466                            &cached_geom.transform(),
467                        );
468
469                        if rendering_dirty {
470                            self.mark_dirty_rect(
471                                cached_geom.bounding_rect(),
472                                state.transform_to_screen,
473                                &state.clipped,
474                            );
475
476                            ItemVisitorResult::Continue(new_state)
477                        } else {
478                            if state.must_refresh_children
479                                || new_state.transform_to_screen
480                                    != new_state.old_transform_to_screen
481                            {
482                                self.mark_dirty_rect(
483                                    cached_geom.bounding_rect(),
484                                    state.old_transform_to_screen,
485                                    &state.clipped,
486                                );
487                                self.mark_dirty_rect(
488                                    cached_geom.bounding_rect(),
489                                    state.transform_to_screen,
490                                    &state.clipped,
491                                );
492                            } else if let Some(tr) = &tracker {
493                                tr.as_ref().register_as_dependency_to_current_binding();
494                            }
495
496                            if let CachedItemBoundingBoxAndTransform::ClipItem { geometry } =
497                                &cached_geom
498                            {
499                                new_state.clipped = new_state
500                                    .clipped
501                                    .intersection(
502                                        &state
503                                            .transform_to_screen
504                                            .outer_transformed_rect(&geometry.cast())
505                                            .cast()
506                                            .union(
507                                                &state
508                                                    .old_transform_to_screen
509                                                    .outer_transformed_rect(&geometry.cast())
510                                                    .cast(),
511                                            ),
512                                    )
513                                    .unwrap_or_default();
514                                if new_state.clipped.is_empty() {
515                                    return ItemVisitorResult::SkipChildren;
516                                }
517                            }
518                            ItemVisitorResult::Continue(new_state)
519                        }
520                    }
521                    None => {
522                        let geom = CachedItemBoundingBoxAndTransform::new::<T>(
523                            &item_rc,
524                            &self.window_adapter,
525                        );
526                        let cache_entry = PartialRenderingCachedData::new(geom.clone());
527                        rendering_data.cache_index.set(cache.insert(cache_entry));
528                        rendering_data.cache_generation.set(cache.generation());
529
530                        new_state.adjust_transforms_for_child(&geom.transform(), &geom.transform());
531
532                        if let CachedItemBoundingBoxAndTransform::ClipItem { geometry } = geom {
533                            new_state.clipped = new_state
534                                .clipped
535                                .intersection(
536                                    &state
537                                        .transform_to_screen
538                                        .outer_transformed_rect(&geometry.cast())
539                                        .cast(),
540                                )
541                                .unwrap_or_default();
542                        }
543
544                        self.mark_dirty_rect(
545                            geom.bounding_rect(),
546                            state.transform_to_screen,
547                            &state.clipped,
548                        );
549                        if new_state.clipped.is_empty() {
550                            ItemVisitorResult::SkipChildren
551                        } else {
552                            ItemVisitorResult::Continue(new_state)
553                        }
554                    }
555                }
556            },
557            {
558                let initial_transform =
559                    euclid::Transform2D::translation(origin.x as f32, origin.y as f32);
560                ComputeDirtyRegionState {
561                    transform_to_screen: initial_transform,
562                    old_transform_to_screen: initial_transform,
563                    clipped: LogicalRect::from_size(size),
564                    must_refresh_children: false,
565                }
566            },
567        );
568    }
569
570    fn mark_dirty_rect(
571        &mut self,
572        rect: &LogicalRect,
573        transform: ItemTransform,
574        clip_rect: &LogicalRect,
575    ) {
576        #[cfg(not(slint_int_coord))]
577        if !rect.origin.is_finite() {
578            // Account for NaN
579            return;
580        }
581
582        if !rect.is_empty() {
583            if let Some(rect) =
584                transform.outer_transformed_rect(&rect.cast()).cast().intersection(clip_rect)
585            {
586                self.dirty_region.add_rect(rect);
587            }
588        }
589    }
590
591    fn do_rendering(
592        cache: &RefCell<PartialRendererCache>,
593        rendering_data: &CachedRenderingData,
594        item_rc: &ItemRc,
595        render_fn: impl FnOnce(),
596    ) {
597        let mut cache = cache.borrow_mut();
598        if let Some(entry) = rendering_data.get_entry(&mut cache) {
599            entry
600                .tracker
601                .get_or_insert_with(|| Box::pin(PropertyTracker::default()))
602                .as_ref()
603                .evaluate(render_fn);
604        } else {
605            // This item was created between the computation of the dirty region and the actual rendering.
606            // Register a dependency to the geometry since this wasn't done before
607            item_rc.geometry();
608            render_fn();
609        }
610    }
611
612    /// Move the actual renderer
613    pub fn into_inner(self) -> T {
614        self.actual_renderer
615    }
616}
617
618macro_rules! forward_rendering_call {
619    (fn $fn:ident($Ty:ty) $(-> $Ret:ty)?) => {
620        fn $fn(&mut self, obj: Pin<&$Ty>, item_rc: &ItemRc, size: LogicalSize) $(-> $Ret)? {
621            let mut ret = None;
622            Self::do_rendering(&self.cache, &obj.cached_rendering_data, item_rc, || {
623                ret = Some(self.actual_renderer.$fn(obj, item_rc, size));
624            });
625            ret.unwrap_or_default()
626        }
627    };
628}
629
630macro_rules! forward_rendering_call2 {
631    (fn $fn:ident($Ty:ty) $(-> $Ret:ty)?) => {
632        fn $fn(&mut self, obj: Pin<&$Ty>, item_rc: &ItemRc, size: LogicalSize, cache: &CachedRenderingData) $(-> $Ret)? {
633            let mut ret = None;
634            Self::do_rendering(&self.cache, &cache, item_rc, || {
635                ret = Some(self.actual_renderer.$fn(obj, item_rc, size, &cache));
636            });
637            ret.unwrap_or_default()
638        }
639    };
640}
641
642impl<T: ItemRenderer + ItemRendererFeatures> ItemRenderer for PartialRenderer<'_, T> {
643    fn filter_item(
644        &mut self,
645        item_rc: &ItemRc,
646        window_adapter: &Rc<dyn WindowAdapter>,
647    ) -> (bool, LogicalRect) {
648        let item = item_rc.borrow();
649
650        // Query untracked, as the bounding rect calculation already registers a dependency on the geometry.
651        let item_geometry = crate::properties::evaluate_no_tracking(|| item_rc.geometry());
652
653        let rendering_data = item.cached_rendering_data_offset();
654        let mut cache = self.cache.borrow_mut();
655        let item_bounding_rect = match rendering_data.get_entry(&mut cache) {
656            Some(PartialRenderingCachedData { data, tracker: _ }) => *data.bounding_rect(),
657            None => {
658                // This item was created between the computation of the dirty region and the actual rendering.
659                item_rc.bounding_rect(&item_geometry, window_adapter)
660            }
661        };
662
663        let clipped_geom = self.get_current_clip().intersection(&item_bounding_rect);
664        let draw = clipped_geom.is_some_and(|clipped_geom| {
665            let clipped_geom = clipped_geom.translate(self.translation());
666            self.dirty_region.draw_intersects(clipped_geom)
667        });
668
669        (draw, item_geometry)
670    }
671
672    forward_rendering_call2!(fn draw_rectangle(dyn RenderRectangle));
673    forward_rendering_call2!(fn draw_border_rectangle(dyn RenderBorderRectangle));
674    forward_rendering_call2!(fn draw_window_background(dyn RenderRectangle));
675    forward_rendering_call2!(fn draw_image(dyn RenderImage));
676    forward_rendering_call2!(fn draw_text(dyn RenderText));
677    forward_rendering_call!(fn draw_text_input(TextInput));
678    #[cfg(feature = "std")]
679    forward_rendering_call!(fn draw_path(Path));
680    forward_rendering_call!(fn draw_box_shadow(BoxShadow));
681
682    forward_rendering_call!(fn visit_clip(Clip) -> RenderingResult);
683    forward_rendering_call!(fn visit_opacity(Opacity) -> RenderingResult);
684
685    fn combine_clip(
686        &mut self,
687        rect: LogicalRect,
688        radius: LogicalBorderRadius,
689        border_width: LogicalLength,
690    ) -> bool {
691        self.actual_renderer.combine_clip(rect, radius, border_width)
692    }
693
694    fn get_current_clip(&self) -> LogicalRect {
695        self.actual_renderer.get_current_clip()
696    }
697
698    fn translate(&mut self, distance: LogicalVector) {
699        self.actual_renderer.translate(distance)
700    }
701    fn translation(&self) -> LogicalVector {
702        self.actual_renderer.translation()
703    }
704
705    fn rotate(&mut self, angle_in_degrees: f32) {
706        self.actual_renderer.rotate(angle_in_degrees)
707    }
708
709    fn scale(&mut self, x_factor: f32, y_factor: f32) {
710        self.actual_renderer.scale(x_factor, y_factor)
711    }
712
713    fn apply_opacity(&mut self, opacity: f32) {
714        self.actual_renderer.apply_opacity(opacity)
715    }
716
717    fn save_state(&mut self) {
718        self.actual_renderer.save_state()
719    }
720
721    fn restore_state(&mut self) {
722        self.actual_renderer.restore_state()
723    }
724
725    fn scale_factor(&self) -> f32 {
726        self.actual_renderer.scale_factor()
727    }
728
729    fn draw_cached_pixmap(
730        &mut self,
731        item_rc: &ItemRc,
732        update_fn: &dyn Fn(&mut dyn FnMut(u32, u32, &[u8])),
733    ) {
734        self.actual_renderer.draw_cached_pixmap(item_rc, update_fn)
735    }
736
737    fn draw_string(&mut self, string: &str, color: crate::Color) {
738        self.actual_renderer.draw_string(string, color)
739    }
740
741    fn draw_image_direct(&mut self, image: crate::graphics::image::Image) {
742        self.actual_renderer.draw_image_direct(image)
743    }
744
745    fn window(&self) -> &crate::window::WindowInner {
746        self.actual_renderer.window()
747    }
748
749    fn as_any(&mut self) -> Option<&mut dyn core::any::Any> {
750        self.actual_renderer.as_any()
751    }
752}
753
754/// This struct holds the state of the partial renderer between different frames, in particular the cache of the bounding rect
755/// of each item. This permits a more fine-grained computation of the region that needs to be repainted.
756#[derive(Default)]
757pub struct PartialRenderingState {
758    partial_cache: RefCell<PartialRendererCache>,
759    /// This is the area which we are going to redraw in the next frame, no matter if the items are dirty or not
760    force_dirty: RefCell<DirtyRegion>,
761    /// Force a redraw in the next frame, no matter what's dirty. Use only as a last resort.
762    force_screen_refresh: Cell<bool>,
763}
764
765impl PartialRenderingState {
766    /// Creates a partial renderer that's initialized with the partial rendering caches maintained in this state structure.
767    /// Call [`Self::apply_dirty_region`] after this function to compute the correct partial rendering region.
768    pub fn create_partial_renderer<T: ItemRenderer + ItemRendererFeatures>(
769        &self,
770        renderer: T,
771    ) -> PartialRenderer<'_, T> {
772        PartialRenderer::new(&self.partial_cache, self.force_dirty.take(), renderer)
773    }
774
775    /// Compute the correct partial rendering region based on the components to be drawn, the bounding rectangles of
776    /// changes items within, and the current repaint buffer type. Returns the computed dirty region just for this frame.
777    /// The provided buffer_dirty_region specifies which area of the buffer is known to *additionally* require repainting,
778    /// where `None` means that buffer is not known to be dirty beyond what applies to this frame (reused buffer).
779    pub fn apply_dirty_region<T: ItemRenderer + ItemRendererFeatures>(
780        &self,
781        partial_renderer: &mut PartialRenderer<'_, T>,
782        components: &[(ItemTreeWeak, LogicalPoint)],
783        logical_window_size: LogicalSize,
784        dirty_region_of_existing_buffer: Option<DirtyRegion>,
785    ) -> DirtyRegion {
786        for (component, origin) in components {
787            if let Some(component) = crate::item_tree::ItemTreeWeak::upgrade(component) {
788                partial_renderer.compute_dirty_regions(&component, *origin, logical_window_size);
789            }
790        }
791
792        let screen_region = LogicalRect::from_size(logical_window_size);
793
794        if self.force_screen_refresh.take() {
795            partial_renderer.dirty_region = screen_region.into();
796        }
797
798        let region_to_repaint = partial_renderer.dirty_region.clone();
799
800        partial_renderer.dirty_region = match dirty_region_of_existing_buffer {
801            Some(dirty_region) => partial_renderer.dirty_region.union(&dirty_region),
802            None => partial_renderer.dirty_region.clone(),
803        }
804        .intersection(screen_region);
805
806        region_to_repaint
807    }
808
809    /// Add the specified region to the list of regions to include in the next rendering.
810    pub fn mark_dirty_region(&self, region: DirtyRegion) {
811        self.force_dirty.replace_with(|r| r.union(&region));
812    }
813
814    /// Call this from your renderer's `free_graphics_resources` function to ensure that the cached item geometries
815    /// are cleared for the destroyed items in the item tree.
816    pub fn free_graphics_resources(&self, items: &mut dyn Iterator<Item = Pin<ItemRef<'_>>>) {
817        for item in items {
818            item.cached_rendering_data_offset().release(&mut self.partial_cache.borrow_mut());
819        }
820
821        // We don't have a way to determine the screen region of the delete items, what's in the cache is relative. So
822        // as a last resort, refresh everything.
823        self.force_screen_refresh.set(true)
824    }
825
826    /// Clears the partial rendering cache. Use this for example when the entire underlying window surface changes.
827    pub fn clear_cache(&self) {
828        self.partial_cache.borrow_mut().clear();
829    }
830
831    /// Force re-rendering of the entire window region the next time a partial renderer is created.
832    pub fn force_screen_refresh(&self) {
833        self.force_screen_refresh.set(true);
834    }
835}
836
837#[test]
838fn dirty_region_no_intersection() {
839    let mut region = DirtyRegion::default();
840    region.add_rect(LogicalRect::new(LogicalPoint::new(10., 10.), LogicalSize::new(16., 16.)));
841    region.add_rect(LogicalRect::new(LogicalPoint::new(100., 100.), LogicalSize::new(16., 16.)));
842    region.add_rect(LogicalRect::new(LogicalPoint::new(200., 100.), LogicalSize::new(16., 16.)));
843    let i = region
844        .intersection(LogicalRect::new(LogicalPoint::new(50., 50.), LogicalSize::new(10., 10.)));
845    assert_eq!(i.iter().count(), 0);
846}