Skip to main content

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