Skip to main content

i_slint_core/
item_rendering.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#![warn(missing_docs)]
5//! module for rendering the tree of items
6
7use super::items::*;
8use crate::graphics::{Color, FontRequest, Image, IntRect};
9use crate::item_tree::ItemTreeRc;
10use crate::item_tree::{ItemVisitor, ItemVisitorVTable, VisitChildrenResult};
11use crate::lengths::{
12    LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, LogicalVector,
13};
14pub use crate::partial_renderer::CachedRenderingData;
15use crate::window::WindowAdapterRc;
16use crate::{Brush, SharedString};
17#[cfg(feature = "std")]
18use alloc::boxed::Box;
19#[cfg(feature = "std")]
20use core::cell::RefCell;
21use core::pin::Pin;
22#[cfg(feature = "std")]
23use std::collections::HashMap;
24use vtable::VRc;
25
26/// A per-item cache.
27///
28/// Cache rendering information for a given item.
29///
30/// Use [`ItemCache::get_or_update_cache_entry`] to get or update the items, the
31/// cache is automatically invalided when the property gets dirty.
32/// [`ItemCache::component_destroyed`] must be called to clear the cache for that
33/// component.
34#[cfg(feature = "std")]
35pub struct ItemCache<T> {
36    /// The pointer is a pointer to a component
37    map: RefCell<HashMap<*const vtable::Dyn, HashMap<u32, crate::graphics::CachedGraphicsData<T>>>>,
38    /// Track if the window scale factor changes; used to clear the cache if necessary.
39    window_scale_factor_tracker: Pin<Box<crate::properties::PropertyTracker>>,
40}
41
42#[cfg(feature = "std")]
43impl<T> Default for ItemCache<T> {
44    fn default() -> Self {
45        Self { map: Default::default(), window_scale_factor_tracker: Box::pin(Default::default()) }
46    }
47}
48
49#[cfg(feature = "std")]
50impl<T: Clone> ItemCache<T> {
51    /// Returns the cached value associated to the `item_rc` if it is still valid.
52    /// Otherwise call the `update_fn` to compute that value, and track property access
53    /// so it is automatically invalided when property becomes dirty.
54    pub fn get_or_update_cache_entry(&self, item_rc: &ItemRc, update_fn: impl FnOnce() -> T) -> T {
55        let component = &(**item_rc.item_tree()) as *const _;
56        let mut borrowed = self.map.borrow_mut();
57        match borrowed.entry(component).or_default().entry(item_rc.index()) {
58            std::collections::hash_map::Entry::Occupied(mut entry) => {
59                let mut tracker = entry.get_mut().dependency_tracker.take();
60                drop(borrowed);
61                let maybe_new_data = tracker
62                    .get_or_insert_with(|| Box::pin(Default::default()))
63                    .as_ref()
64                    .evaluate_if_dirty(update_fn);
65                let mut borrowed = self.map.borrow_mut();
66                let e = borrowed.get_mut(&component).unwrap().get_mut(&item_rc.index()).unwrap();
67                e.dependency_tracker = tracker;
68                if let Some(new_data) = maybe_new_data {
69                    e.data = new_data.clone();
70                    new_data
71                } else {
72                    e.data.clone()
73                }
74            }
75            std::collections::hash_map::Entry::Vacant(_) => {
76                drop(borrowed);
77                let new_entry = crate::graphics::CachedGraphicsData::new(update_fn);
78                let data = new_entry.data.clone();
79                self.map
80                    .borrow_mut()
81                    .get_mut(&component)
82                    .unwrap()
83                    .insert(item_rc.index(), new_entry);
84                data
85            }
86        }
87    }
88}
89
90#[cfg(feature = "std")]
91impl<T> ItemCache<T> {
92    /// Returns the cached value associated with the `item_rc` if it is in the cache
93    /// and still valid.
94    pub fn with_entry<U>(
95        &self,
96        item_rc: &ItemRc,
97        callback: impl FnOnce(&T) -> Option<U>,
98    ) -> Option<U> {
99        let component = &(**item_rc.item_tree()) as *const _;
100        self.map
101            .borrow()
102            .get(&component)
103            .and_then(|per_component_entries| per_component_entries.get(&item_rc.index()))
104            .and_then(|entry| callback(&entry.data))
105    }
106
107    /// Clears the cache if the window's scale factor has changed since the last call.
108    pub fn clear_cache_if_scale_factor_changed(&self, window: &crate::api::Window) {
109        if self.window_scale_factor_tracker.is_dirty() {
110            self.window_scale_factor_tracker
111                .as_ref()
112                .evaluate_as_dependency_root(|| window.scale_factor());
113            self.clear_all();
114        }
115    }
116
117    /// free the whole cache
118    pub fn clear_all(&self) {
119        self.map.borrow_mut().clear();
120    }
121
122    /// Function that must be called when a component is destroyed.
123    ///
124    /// Usually can be called from [`crate::window::WindowAdapterInternal::unregister_item_tree`]
125    pub fn component_destroyed(&self, component: crate::item_tree::ItemTreeRef) {
126        let component_ptr: *const _ =
127            crate::item_tree::ItemTreeRef::as_ptr(component).cast().as_ptr();
128        self.map.borrow_mut().remove(&component_ptr);
129    }
130
131    /// free the cache for a given item
132    pub fn release(&self, item_rc: &ItemRc) {
133        let component = &(**item_rc.item_tree()) as *const _;
134        if let Some(sub) = self.map.borrow_mut().get_mut(&component) {
135            sub.remove(&item_rc.index());
136        }
137    }
138
139    /// Returns true if there are no entries in the cache; false otherwise.
140    pub fn is_empty(&self) -> bool {
141        self.map.borrow().is_empty()
142    }
143
144    /// Returns a [`RefMut`](std::cell::RefMut) referencing the cached value associated with
145    /// `item_rc`, updating the cache entry first if necessary using `update_fn`.
146    ///
147    /// Unlike [`get_or_update_cache_entry`](Self::get_or_update_cache_entry), this method does
148    /// not require `T: Clone` and returns a mutable reference into the cache, which permits
149    /// in-place modification or temporary extraction of the cached value (e.g., via
150    /// [`std::mem::take`]).
151    pub fn get_or_update_cache_entry_ref(
152        &self,
153        item_rc: &ItemRc,
154        update_fn: impl FnOnce() -> T,
155    ) -> std::cell::RefMut<'_, T> {
156        let component = &(**item_rc.item_tree()) as *const _;
157        let index = item_rc.index();
158
159        {
160            let mut borrowed = self.map.borrow_mut();
161            match borrowed.entry(component).or_default().entry(index) {
162                std::collections::hash_map::Entry::Occupied(mut entry) => {
163                    let mut tracker = entry.get_mut().dependency_tracker.take();
164                    drop(borrowed);
165                    let maybe_new_data = tracker
166                        .get_or_insert_with(|| Box::pin(Default::default()))
167                        .as_ref()
168                        .evaluate_if_dirty(update_fn);
169                    let mut borrowed = self.map.borrow_mut();
170                    let e = borrowed.get_mut(&component).unwrap().get_mut(&index).unwrap();
171                    e.dependency_tracker = tracker;
172                    if let Some(new_data) = maybe_new_data {
173                        e.data = new_data;
174                    }
175                }
176                std::collections::hash_map::Entry::Vacant(_) => {
177                    drop(borrowed);
178                    let new_entry = crate::graphics::CachedGraphicsData::new(update_fn);
179                    self.map.borrow_mut().get_mut(&component).unwrap().insert(index, new_entry);
180                }
181            }
182        }
183
184        std::cell::RefMut::map(self.map.borrow_mut(), |map| {
185            &mut map.get_mut(&component).unwrap().get_mut(&index).unwrap().data
186        })
187    }
188}
189
190/// Renders the children of the item with the specified index into the renderer.
191pub fn render_item_children(
192    renderer: &mut dyn ItemRenderer,
193    component: &ItemTreeRc,
194    index: isize,
195    window_adapter: &WindowAdapterRc,
196) {
197    let mut actual_visitor =
198        |component: &ItemTreeRc, index: u32, item: Pin<ItemRef>| -> VisitChildrenResult {
199            renderer.save_state();
200            let item_rc = ItemRc::new(component.clone(), index);
201
202            let (do_draw, item_geometry) = renderer.filter_item(&item_rc, window_adapter);
203
204            let item_origin = item_geometry.origin;
205            renderer.translate(item_origin.to_vector());
206
207            // Don't render items that are clipped, with the exception of the Clip or Flickable since
208            // they themselves clip their content.
209            let render_result = if do_draw
210               || item.as_ref().clips_children()
211               // HACK, the geometry of the box shadow does not include the shadow, because when the shadow is the root for repeated elements it would translate the children
212               || ItemRef::downcast_pin::<BoxShadow>(item).is_some()
213               // Transform and Opacity should also be applied regardless if the item itself is clipped or not
214               || ItemRef::downcast_pin::<Transform>(item).is_some()
215               || ItemRef::downcast_pin::<Opacity>(item).is_some()
216               || ItemRef::downcast_pin::<Layer>(item).is_some()
217            {
218                item.as_ref().render(
219                    &mut (renderer as &mut dyn ItemRenderer),
220                    &item_rc,
221                    item_geometry.size,
222                )
223            } else {
224                RenderingResult::ContinueRenderingChildren
225            };
226
227            if matches!(render_result, RenderingResult::ContinueRenderingChildren) {
228                render_item_children(renderer, component, index as isize, window_adapter);
229            }
230            renderer.restore_state();
231            VisitChildrenResult::CONTINUE
232        };
233    vtable::new_vref!(let mut actual_visitor : VRefMut<ItemVisitorVTable> for ItemVisitor = &mut actual_visitor);
234    VRc::borrow_pin(component).as_ref().visit_children_item(
235        index,
236        crate::item_tree::TraversalOrder::BackToFront,
237        actual_visitor,
238    );
239}
240
241/// Renders the tree of items that component holds, using the specified renderer. Rendering is done
242/// relative to the specified origin.
243pub fn render_component_items(
244    component: &ItemTreeRc,
245    renderer: &mut dyn ItemRenderer,
246    origin: LogicalPoint,
247    window_adapter: &WindowAdapterRc,
248) {
249    renderer.save_state();
250    renderer.translate(origin.to_vector());
251
252    render_item_children(renderer, component, -1, window_adapter);
253
254    renderer.restore_state();
255}
256
257/// Compute the bounding rect of all children. This does /not/ include item's own bounding rect. Remember to run this
258/// via `evaluate_no_tracking`.
259pub fn item_children_bounding_rect(
260    item_rc: &ItemRc,
261    window_adapter: &WindowAdapterRc,
262) -> LogicalRect {
263    item_children_bounding_rect_transformed(item_rc, window_adapter, Default::default())
264}
265
266fn item_children_bounding_rect_transformed(
267    item_rc: &ItemRc,
268    window_adapter: &WindowAdapterRc,
269    transform: crate::lengths::ItemTransform,
270) -> LogicalRect {
271    let mut bounding_rect = LogicalRect::zero();
272
273    let mut actual_visitor =
274        |component: &ItemTreeRc, index: u32, _ref: Pin<ItemRef>| -> VisitChildrenResult {
275            let item_rc = ItemRc::new(component.clone(), index);
276            let bounds_with_children =
277                item_with_children_bounding_rect_transformed(&item_rc, window_adapter, transform);
278
279            bounding_rect = bounding_rect.union(&bounds_with_children);
280
281            VisitChildrenResult::CONTINUE
282        };
283
284    vtable::new_vref!(let mut actual_visitor : VRefMut<ItemVisitorVTable> for ItemVisitor = &mut actual_visitor);
285    VRc::borrow_pin(item_rc.item_tree()).as_ref().visit_children_item(
286        item_rc.index() as isize,
287        crate::item_tree::TraversalOrder::BackToFront,
288        actual_visitor,
289    );
290
291    bounding_rect
292}
293
294fn item_with_children_bounding_rect_transformed(
295    item_rc: &ItemRc,
296    window_adapter: &WindowAdapterRc,
297    transform: crate::lengths::ItemTransform,
298) -> LogicalRect {
299    let item_geom = item_rc.geometry();
300
301    if item_rc.borrow().as_ref().clips_children() {
302        transform.outer_transformed_rect(&item_geom.cast()).cast()
303    } else {
304        let bounding = item_rc.bounding_rect(&item_geom, window_adapter);
305        let bounding = transform.outer_transformed_rect(&bounding.cast());
306        let children_relative_transform = item_rc
307            .children_transform()
308            .unwrap_or_default()
309            .then_translate(item_geom.origin.to_vector().cast());
310
311        let children_absolute_transform = transform.then(&children_relative_transform);
312
313        item_children_bounding_rect_transformed(
314            item_rc,
315            window_adapter,
316            children_absolute_transform,
317        )
318        .union(&bounding.cast())
319    }
320}
321
322/// Trait for an item that represent a Rectangle to the Renderer
323#[allow(missing_docs)]
324pub trait RenderRectangle {
325    fn background(self: Pin<&Self>) -> Brush;
326}
327
328/// Trait for an item that represent a Rectangle with a border to the Renderer
329#[allow(missing_docs)]
330pub trait RenderBorderRectangle {
331    fn background(self: Pin<&Self>) -> Brush;
332    fn border_width(self: Pin<&Self>) -> LogicalLength;
333    fn border_radius(self: Pin<&Self>) -> LogicalBorderRadius;
334    fn border_color(self: Pin<&Self>) -> Brush;
335}
336
337/// Trait for an item that represents an Image towards the renderer
338#[allow(missing_docs)]
339pub trait RenderImage {
340    fn target_size(self: Pin<&Self>) -> LogicalSize;
341    fn source(self: Pin<&Self>) -> Image;
342    fn source_clip(self: Pin<&Self>) -> Option<IntRect>;
343    fn image_fit(self: Pin<&Self>) -> ImageFit;
344    fn rendering(self: Pin<&Self>) -> ImageRendering;
345    fn colorize(self: Pin<&Self>) -> Brush;
346    fn alignment(self: Pin<&Self>) -> (ImageHorizontalAlignment, ImageVerticalAlignment);
347    fn tiling(self: Pin<&Self>) -> (ImageTiling, ImageTiling);
348}
349
350/// Trait for an item has font properties
351#[allow(missing_docs)]
352pub trait HasFont {
353    fn font_request(self: Pin<&Self>, self_rc: &crate::items::ItemRc) -> FontRequest;
354}
355
356#[allow(missing_docs)]
357pub enum PlainOrStyledText {
358    Plain(SharedString),
359    Styled(crate::styled_text::StyledText),
360}
361
362/// Trait for an item that represents an string towards the renderer
363#[allow(missing_docs)]
364pub trait RenderString: HasFont {
365    fn text(self: Pin<&Self>) -> PlainOrStyledText;
366}
367
368/// Trait for an item that represents an Text towards the renderer
369#[allow(missing_docs)]
370pub trait RenderText: RenderString {
371    fn target_size(self: Pin<&Self>) -> LogicalSize;
372    fn color(self: Pin<&Self>) -> Brush;
373    fn alignment(self: Pin<&Self>) -> (TextHorizontalAlignment, TextVerticalAlignment);
374    fn wrap(self: Pin<&Self>) -> TextWrap;
375    fn overflow(self: Pin<&Self>) -> TextOverflow;
376    fn stroke(self: Pin<&Self>) -> (Brush, LogicalLength, TextStrokeStyle);
377    fn is_markdown(self: Pin<&Self>) -> bool;
378    fn link_color(self: Pin<&Self>) -> Color;
379}
380
381impl HasFont for (SharedString, Brush) {
382    fn font_request(self: Pin<&Self>, self_rc: &crate::items::ItemRc) -> FontRequest {
383        crate::items::WindowItem::resolved_font_request(
384            self_rc,
385            SharedString::default(),
386            0,
387            LogicalLength::default(),
388            LogicalLength::default(),
389            false,
390        )
391    }
392}
393
394impl RenderString for (SharedString, Brush) {
395    fn text(self: Pin<&Self>) -> PlainOrStyledText {
396        PlainOrStyledText::Plain(self.0.clone())
397    }
398}
399
400impl RenderText for (SharedString, Brush) {
401    fn target_size(self: Pin<&Self>) -> LogicalSize {
402        LogicalSize::default()
403    }
404
405    fn color(self: Pin<&Self>) -> Brush {
406        self.1.clone()
407    }
408
409    fn link_color(self: Pin<&Self>) -> Color {
410        Default::default()
411    }
412
413    fn alignment(
414        self: Pin<&Self>,
415    ) -> (crate::items::TextHorizontalAlignment, crate::items::TextVerticalAlignment) {
416        Default::default()
417    }
418
419    fn wrap(self: Pin<&Self>) -> crate::items::TextWrap {
420        Default::default()
421    }
422
423    fn overflow(self: Pin<&Self>) -> crate::items::TextOverflow {
424        Default::default()
425    }
426
427    fn stroke(self: Pin<&Self>) -> (Brush, LogicalLength, TextStrokeStyle) {
428        Default::default()
429    }
430
431    fn is_markdown(self: Pin<&Self>) -> bool {
432        false
433    }
434}
435
436/// Trait used to render each items.
437///
438/// The item needs to be rendered relative to its (x,y) position. For example,
439/// draw_rectangle should draw a rectangle in `(pos.x + rect.x, pos.y + rect.y)`
440#[allow(missing_docs)]
441pub trait ItemRenderer {
442    fn draw_rectangle(
443        &mut self,
444        rect: Pin<&dyn RenderRectangle>,
445        _self_rc: &ItemRc,
446        _size: LogicalSize,
447        _cache: &CachedRenderingData,
448    );
449    fn draw_border_rectangle(
450        &mut self,
451        rect: Pin<&dyn RenderBorderRectangle>,
452        _self_rc: &ItemRc,
453        _size: LogicalSize,
454        _cache: &CachedRenderingData,
455    );
456    fn draw_window_background(
457        &mut self,
458        rect: Pin<&dyn RenderRectangle>,
459        self_rc: &ItemRc,
460        size: LogicalSize,
461        cache: &CachedRenderingData,
462    );
463    fn draw_image(
464        &mut self,
465        image: Pin<&dyn RenderImage>,
466        _self_rc: &ItemRc,
467        _size: LogicalSize,
468        _cache: &CachedRenderingData,
469    );
470    fn draw_text(
471        &mut self,
472        text: Pin<&dyn RenderText>,
473        _self_rc: &ItemRc,
474        _size: LogicalSize,
475        _cache: &CachedRenderingData,
476    );
477    fn draw_text_input(
478        &mut self,
479        text_input: Pin<&TextInput>,
480        _self_rc: &ItemRc,
481        _size: LogicalSize,
482    );
483    #[cfg(feature = "path")]
484    fn draw_path(&mut self, path: Pin<&Path>, _self_rc: &ItemRc, _size: LogicalSize);
485    fn draw_box_shadow(
486        &mut self,
487        box_shadow: Pin<&BoxShadow>,
488        _self_rc: &ItemRc,
489        _size: LogicalSize,
490    );
491    fn visit_opacity(
492        &mut self,
493        opacity_item: Pin<&Opacity>,
494        _self_rc: &ItemRc,
495        _size: LogicalSize,
496    ) -> RenderingResult {
497        self.apply_opacity(opacity_item.opacity());
498        RenderingResult::ContinueRenderingChildren
499    }
500    fn visit_layer(
501        &mut self,
502        _layer_item: Pin<&Layer>,
503        _self_rc: &ItemRc,
504        _size: LogicalSize,
505    ) -> RenderingResult {
506        // Not supported
507        RenderingResult::ContinueRenderingChildren
508    }
509
510    // Apply the bounds of the Clip element, if enabled. The default implementation calls
511    // combine_clip, but the render may choose an alternate way of implementing the clip.
512    // For example the GL backend uses a layered rendering approach.
513    fn visit_clip(
514        &mut self,
515        clip_item: Pin<&Clip>,
516        _item_rc: &ItemRc,
517        size: LogicalSize,
518    ) -> RenderingResult {
519        if clip_item.clip() {
520            let clip_region_valid = self.combine_clip(
521                LogicalRect::new(LogicalPoint::default(), size),
522                clip_item.logical_border_radius(),
523                clip_item.border_width(),
524            );
525
526            // If clipping is enabled but the clip element is outside the visible range, then we don't
527            // need to bother doing anything, not even rendering the children.
528            if !clip_region_valid {
529                return RenderingResult::ContinueRenderingWithoutChildren;
530            }
531        }
532        RenderingResult::ContinueRenderingChildren
533    }
534
535    /// Clip the further call until restore_state.
536    /// radius/border_width can be used for border rectangle clip.
537    /// (FIXME: consider removing radius/border_width and have another  function that take a path instead)
538    /// Returns a boolean indicating the state of the new clip region: true if the clip region covers
539    /// an area; false if the clip region is empty.
540    fn combine_clip(
541        &mut self,
542        rect: LogicalRect,
543        radius: LogicalBorderRadius,
544        border_width: LogicalLength,
545    ) -> bool;
546    /// Get the current clip bounding box in the current transformed coordinate.
547    fn get_current_clip(&self) -> LogicalRect;
548
549    fn translate(&mut self, distance: LogicalVector);
550    fn translation(&self) -> LogicalVector {
551        unimplemented!()
552    }
553    fn rotate(&mut self, angle_in_degrees: f32);
554    fn scale(&mut self, scale_x_factor: f32, scale_y_factor: f32);
555    /// Apply the opacity (between 0 and 1) for all following items until the next call to restore_state.
556    fn apply_opacity(&mut self, opacity: f32);
557
558    fn save_state(&mut self);
559    fn restore_state(&mut self);
560
561    /// Returns the scale factor
562    fn scale_factor(&self) -> f32;
563
564    /// Draw a pixmap in position indicated by the `pos`.
565    /// The pixmap will be taken from cache if the cache is valid, otherwise, update_fn will be called
566    /// with a callback that need to be called once with `fn (width, height, data)` where data are the
567    /// RGBA premultiplied pixel values
568    fn draw_cached_pixmap(
569        &mut self,
570        item_cache: &ItemRc,
571        update_fn: &dyn Fn(&mut dyn FnMut(u32, u32, &[u8])),
572    );
573
574    /// Draw the given string with the specified color at current (0, 0) with the default font. Mainly
575    /// used by the performance counter overlay.
576    fn draw_string(&mut self, string: &str, color: crate::Color);
577
578    fn draw_image_direct(&mut self, image: crate::graphics::Image);
579
580    /// This is called before it is being rendered (before the draw_* function).
581    /// Returns
582    ///  - if the item needs to be drawn (false means it is clipped or doesn't need to be drawn)
583    ///  - the geometry of the item
584    fn filter_item(
585        &mut self,
586        item: &ItemRc,
587        window_adapter: &WindowAdapterRc,
588    ) -> (bool, LogicalRect) {
589        let item_geometry = item.geometry();
590        // Query bounding rect untracked, as properties that affect the bounding rect are already tracked
591        // when rendering the item.
592        let bounding_rect = crate::properties::evaluate_no_tracking(|| {
593            item.bounding_rect(&item_geometry, window_adapter)
594        });
595        (self.get_current_clip().intersects(&bounding_rect), item_geometry)
596    }
597
598    fn window(&self) -> &crate::window::WindowInner;
599
600    /// Return the internal renderer
601    fn as_any(&mut self) -> Option<&mut dyn core::any::Any>;
602}
603
604/// Helper trait to express the features of an item renderer.
605pub trait ItemRendererFeatures {
606    /// The renderer supports applying 2D transformations to items.
607    const SUPPORTS_TRANSFORMATIONS: bool;
608}