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