gpui_component/
virtual_list.rs

1//! Vistual List for render a large number of differently sized rows/columns.
2//!
3//! > NOTE: This must ensure each column width or row height.
4//!
5//! Only visible range are rendered for performance reasons.
6//!
7//! Inspired by `gpui::uniform_list`.
8//! https://github.com/zed-industries/zed/blob/0ae1603610ab6b265bdfbee7b8dbc23c5ab06edc/crates/gpui/src/elements/uniform_list.rs
9//!
10//! Unlike the `uniform_list`, the each item can have different size.
11//!
12//! This is useful for more complex layout, for example, a table with different row height.
13use std::{
14    cell::RefCell,
15    cmp,
16    ops::{Deref, Range},
17    rc::Rc,
18};
19
20use gpui::{
21    div, point, px, size, Along, AnyElement, App, AvailableSpace, Axis, Bounds, ContentMask,
22    Context, DeferredScrollToItem, Div, Element, ElementId, Entity, GlobalElementId, Half, Hitbox,
23    InteractiveElement, IntoElement, IsZero as _, ListSizingBehavior, Pixels, Point, Render,
24    ScrollHandle, ScrollStrategy, Size, Stateful, StatefulInteractiveElement, StyleRefinement,
25    Styled, Window,
26};
27use smallvec::SmallVec;
28
29use crate::{scroll::ScrollHandleOffsetable, AxisExt, PixelsExt};
30
31struct VirtualListScrollHandleState {
32    axis: Axis,
33    items_count: usize,
34    pub deferred_scroll_to_item: Option<DeferredScrollToItem>,
35}
36
37/// A scroll handle for [`VirtualList`].
38///
39/// See also [`ScrollHandle`].
40#[derive(Clone)]
41pub struct VirtualListScrollHandle {
42    state: Rc<RefCell<VirtualListScrollHandleState>>,
43    base_handle: ScrollHandle,
44}
45
46impl From<ScrollHandle> for VirtualListScrollHandle {
47    fn from(handle: ScrollHandle) -> Self {
48        let mut this = VirtualListScrollHandle::new();
49        this.base_handle = handle;
50        this
51    }
52}
53
54impl AsRef<ScrollHandle> for VirtualListScrollHandle {
55    fn as_ref(&self) -> &ScrollHandle {
56        &self.base_handle
57    }
58}
59
60impl ScrollHandleOffsetable for VirtualListScrollHandle {
61    fn offset(&self) -> Point<Pixels> {
62        self.base_handle.offset()
63    }
64
65    fn set_offset(&self, offset: Point<Pixels>) {
66        self.base_handle.set_offset(offset);
67    }
68
69    fn content_size(&self) -> Size<Pixels> {
70        self.base_handle.content_size()
71    }
72}
73
74impl Deref for VirtualListScrollHandle {
75    type Target = ScrollHandle;
76
77    fn deref(&self) -> &Self::Target {
78        &self.base_handle
79    }
80}
81
82impl VirtualListScrollHandle {
83    /// Create a new VirtualListScrollHandle.
84    pub fn new() -> Self {
85        VirtualListScrollHandle {
86            state: Rc::new(RefCell::new(VirtualListScrollHandleState {
87                axis: Axis::Vertical,
88                items_count: 0,
89                deferred_scroll_to_item: None,
90            })),
91            base_handle: ScrollHandle::default(),
92        }
93    }
94
95    /// Get the base scroll handle.
96    pub fn base_handle(&self) -> &ScrollHandle {
97        &self.base_handle
98    }
99
100    /// Scroll to the item at the given index.
101    pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) {
102        self.scroll_to_item_with_offset(ix, strategy, 0);
103    }
104
105    /// Scroll to the item at the given index, with an additional offset items.
106    fn scroll_to_item_with_offset(&self, ix: usize, strategy: ScrollStrategy, offset: usize) {
107        let mut state = self.state.borrow_mut();
108        state.deferred_scroll_to_item = Some(DeferredScrollToItem {
109            item_index: ix,
110            strategy,
111            offset,
112            scroll_strict: false,
113        });
114    }
115
116    /// Scrolls to the bottom of the list.
117    pub fn scroll_to_bottom(&self) {
118        let items_count = self.state.borrow().items_count;
119        self.scroll_to_item(items_count.saturating_sub(1), ScrollStrategy::Top);
120    }
121}
122
123/// Create a [`VirtualList`] in vertical direction.
124///
125/// This is like `uniform_list` in GPUI, but support two axis.
126///
127/// The `item_sizes` is the size of each column,
128/// only the `height` is used, `width` is ignored and VirtualList will measure the first item width.
129///
130/// See also [`h_virtual_list`]
131#[inline]
132pub fn v_virtual_list<R, V>(
133    view: Entity<V>,
134    id: impl Into<ElementId>,
135    item_sizes: Rc<Vec<Size<Pixels>>>,
136    f: impl 'static + Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> Vec<R>,
137) -> VirtualList
138where
139    R: IntoElement,
140    V: Render,
141{
142    virtual_list(view, id, Axis::Vertical, item_sizes, f)
143}
144
145/// Create a [`VirtualList`] in horizontal direction.
146///
147/// The `item_sizes` is the size of each column,
148/// only the `width` is used, `height` is ignored and VirtualList will measure the first item height.
149///
150/// See also [`v_virtual_list`]
151#[inline]
152pub fn h_virtual_list<R, V>(
153    view: Entity<V>,
154    id: impl Into<ElementId>,
155    item_sizes: Rc<Vec<Size<Pixels>>>,
156    f: impl 'static + Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> Vec<R>,
157) -> VirtualList
158where
159    R: IntoElement,
160    V: Render,
161{
162    virtual_list(view, id, Axis::Horizontal, item_sizes, f)
163}
164
165pub(crate) fn virtual_list<R, V>(
166    view: Entity<V>,
167    id: impl Into<ElementId>,
168    axis: Axis,
169    item_sizes: Rc<Vec<Size<Pixels>>>,
170    f: impl 'static + Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> Vec<R>,
171) -> VirtualList
172where
173    R: IntoElement,
174    V: Render,
175{
176    let id: ElementId = id.into();
177    let scroll_handle = VirtualListScrollHandle::new();
178    let render_range = move |visible_range, window: &mut Window, cx: &mut App| {
179        view.update(cx, |this, cx| {
180            f(this, visible_range, window, cx)
181                .into_iter()
182                .map(|component| component.into_any_element())
183                .collect()
184        })
185    };
186
187    VirtualList {
188        id: id.clone(),
189        axis,
190        base: div()
191            .id(id)
192            .size_full()
193            .overflow_scroll()
194            .track_scroll(&scroll_handle),
195        scroll_handle,
196        items_count: item_sizes.len(),
197        item_sizes,
198        render_items: Box::new(render_range),
199        sizing_behavior: ListSizingBehavior::default(),
200    }
201}
202
203/// VirtualList component for rendering a large number of differently sized items.
204pub struct VirtualList {
205    id: ElementId,
206    axis: Axis,
207    base: Stateful<Div>,
208    scroll_handle: VirtualListScrollHandle,
209    items_count: usize,
210    item_sizes: Rc<Vec<Size<Pixels>>>,
211    render_items: Box<
212        dyn for<'a> Fn(Range<usize>, &'a mut Window, &'a mut App) -> SmallVec<[AnyElement; 64]>,
213    >,
214    sizing_behavior: ListSizingBehavior,
215}
216
217impl Styled for VirtualList {
218    fn style(&mut self) -> &mut StyleRefinement {
219        self.base.style()
220    }
221}
222
223impl VirtualList {
224    pub fn track_scroll(mut self, scroll_handle: &VirtualListScrollHandle) -> Self {
225        self.base = self.base.track_scroll(&scroll_handle);
226        self.scroll_handle = scroll_handle.clone();
227        self
228    }
229
230    /// Set the sizing behavior for the list.
231    pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
232        self.sizing_behavior = behavior;
233        self
234    }
235
236    /// Specify for table.
237    ///
238    /// Table is special, because the `scroll_handle` is based on Table head (That is not a virtual list).
239    pub(crate) fn with_scroll_handle(mut self, scroll_handle: &VirtualListScrollHandle) -> Self {
240        self.base = div().id(self.id.clone()).size_full();
241        self.scroll_handle = scroll_handle.clone();
242        self
243    }
244
245    fn scroll_to_deferred_item(
246        &self,
247        scroll_offset: Point<Pixels>,
248        items_bounds: &[Bounds<Pixels>],
249        content_bounds: &Bounds<Pixels>,
250        scroll_to_item: DeferredScrollToItem,
251    ) -> Point<Pixels> {
252        let Some(bounds) = items_bounds
253            .get(scroll_to_item.item_index + scroll_to_item.offset)
254            .cloned()
255        else {
256            return scroll_offset;
257        };
258
259        let mut scroll_offset = scroll_offset;
260        match scroll_to_item.strategy {
261            ScrollStrategy::Center => {
262                if self.axis.is_vertical() {
263                    scroll_offset.y = content_bounds.top() + content_bounds.size.height.half()
264                        - bounds.top()
265                        - bounds.size.height.half()
266                } else {
267                    scroll_offset.x = content_bounds.left() + content_bounds.size.width.half()
268                        - bounds.left()
269                        - bounds.size.width.half()
270                }
271            }
272            _ => {
273                // Ref: https://github.com/zed-industries/zed/blob/0d145289e0867a8d5d63e5e1397a5ca69c9d49c3/crates/gpui/src/elements/div.rs#L3026
274                if self.axis.is_vertical() {
275                    if bounds.top() + scroll_offset.y < content_bounds.top() {
276                        scroll_offset.y = content_bounds.top() - bounds.top()
277                    } else if bounds.bottom() + scroll_offset.y > content_bounds.bottom() {
278                        scroll_offset.y = content_bounds.bottom() - bounds.bottom();
279                    }
280                } else {
281                    if bounds.left() + scroll_offset.x < content_bounds.left() {
282                        scroll_offset.x = content_bounds.left() - bounds.left();
283                    } else if bounds.right() + scroll_offset.x > content_bounds.right() {
284                        scroll_offset.x = content_bounds.right() - bounds.right();
285                    }
286                }
287            }
288        }
289        self.scroll_handle.set_offset(scroll_offset);
290        scroll_offset
291    }
292
293    /// Ref from: https://github.com/zed-industries/zed/blob/83f9f9d9e3f5914392cab9a09e3472711a1d7b38/crates/gpui/src/elements/uniform_list.rs#L660
294    fn measure_item(
295        &self,
296        list_width: Option<Pixels>,
297        window: &mut Window,
298        cx: &mut App,
299    ) -> Size<Pixels> {
300        if self.items_count == 0 {
301            return Size::default();
302        }
303
304        let item_ix = 0;
305        let mut items = (self.render_items)(item_ix..item_ix + 1, window, cx);
306        let Some(mut item_to_measure) = items.pop() else {
307            return Size::default();
308        };
309        let available_space = size(
310            list_width.map_or(AvailableSpace::MinContent, |width| {
311                AvailableSpace::Definite(width)
312            }),
313            AvailableSpace::MinContent,
314        );
315        item_to_measure.layout_as_root(available_space, window, cx)
316    }
317}
318
319/// Frame state used by the [VirtualItem].
320pub struct VirtualListFrameState {
321    /// Visible items to be painted.
322    items: SmallVec<[AnyElement; 32]>,
323    size_layout: ItemSizeLayout,
324}
325
326#[derive(Default, Clone)]
327pub struct ItemSizeLayout {
328    items_sizes: Rc<Vec<Size<Pixels>>>,
329    content_size: Size<Pixels>,
330    sizes: Vec<Pixels>,
331    origins: Vec<Pixels>,
332    last_layout_bounds: Bounds<Pixels>,
333}
334
335impl IntoElement for VirtualList {
336    type Element = Self;
337
338    fn into_element(self) -> Self::Element {
339        self
340    }
341}
342
343impl Element for VirtualList {
344    type RequestLayoutState = VirtualListFrameState;
345    type PrepaintState = Option<Hitbox>;
346
347    fn id(&self) -> Option<ElementId> {
348        Some(self.id.clone())
349    }
350
351    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
352        None
353    }
354
355    fn request_layout(
356        &mut self,
357        global_id: Option<&GlobalElementId>,
358        inspector_id: Option<&gpui::InspectorElementId>,
359        window: &mut Window,
360        cx: &mut App,
361    ) -> (gpui::LayoutId, Self::RequestLayoutState) {
362        let rem_size = window.rem_size();
363        let font_size = window.text_style().font_size.to_pixels(rem_size);
364        let mut size_layout = ItemSizeLayout::default();
365        let longest_item_size = self.measure_item(None, window, cx);
366
367        let layout_id = self.base.interactivity().request_layout(
368            global_id,
369            inspector_id,
370            window,
371            cx,
372            |style, window, cx| {
373                size_layout = window.with_element_state(
374                    global_id.unwrap(),
375                    |state: Option<ItemSizeLayout>, _window| {
376                        let mut state = state.unwrap_or(ItemSizeLayout::default());
377
378                        // Including the gap between items for calculate the item size
379                        let gap = style
380                            .gap
381                            .along(self.axis)
382                            .to_pixels(font_size.into(), rem_size);
383
384                        if state.items_sizes != self.item_sizes {
385                            state.items_sizes = self.item_sizes.clone();
386                            // Prepare each item's size by axis
387                            state.sizes = self
388                                .item_sizes
389                                .iter()
390                                .enumerate()
391                                .map(|(i, size)| {
392                                    let size = size.along(self.axis);
393                                    if i + 1 == self.items_count {
394                                        size
395                                    } else {
396                                        size + gap
397                                    }
398                                })
399                                .collect::<Vec<_>>();
400
401                            // Prepare each item's origin by axis
402                            state.origins = state
403                                .sizes
404                                .iter()
405                                .scan(px(0.), |cumulative, size| match self.axis {
406                                    Axis::Horizontal => {
407                                        let x = *cumulative;
408                                        *cumulative += *size;
409                                        Some(x)
410                                    }
411                                    Axis::Vertical => {
412                                        let y = *cumulative;
413                                        *cumulative += *size;
414                                        Some(y)
415                                    }
416                                })
417                                .collect::<Vec<_>>();
418
419                            state.content_size = if self.axis.is_horizontal() {
420                                Size {
421                                    width: px(state
422                                        .sizes
423                                        .iter()
424                                        .map(|size| size.as_f32())
425                                        .sum::<f32>()),
426                                    height: longest_item_size.height,
427                                }
428                            } else {
429                                Size {
430                                    width: longest_item_size.width,
431                                    height: px(state
432                                        .sizes
433                                        .iter()
434                                        .map(|size| size.as_f32())
435                                        .sum::<f32>()),
436                                }
437                            };
438                        }
439
440                        (state.clone(), state)
441                    },
442                );
443
444                let axis = self.axis;
445                let layout_id =
446                    match self.sizing_behavior {
447                        ListSizingBehavior::Infer => {
448                            window.with_text_style(style.text_style().cloned(), |window| {
449                                let size_layout = size_layout.clone();
450
451                                window.request_measured_layout(style, {
452                                    move |known_dimensions, available_space, _, _| {
453                                        let mut size = Size::default();
454                                        if axis.is_horizontal() {
455                                            size.width = known_dimensions.width.unwrap_or(
456                                                match available_space.width {
457                                                    AvailableSpace::Definite(x) => x,
458                                                    AvailableSpace::MinContent
459                                                    | AvailableSpace::MaxContent => {
460                                                        size_layout.content_size.width
461                                                    }
462                                                },
463                                            );
464                                            size.height = known_dimensions.width.unwrap_or(
465                                                match available_space.height {
466                                                    AvailableSpace::Definite(x) => x,
467                                                    AvailableSpace::MinContent
468                                                    | AvailableSpace::MaxContent => {
469                                                        size_layout.content_size.height
470                                                    }
471                                                },
472                                            );
473                                        } else {
474                                            size.width = known_dimensions.width.unwrap_or(
475                                                match available_space.width {
476                                                    AvailableSpace::Definite(x) => x,
477                                                    AvailableSpace::MinContent
478                                                    | AvailableSpace::MaxContent => {
479                                                        size_layout.content_size.width
480                                                    }
481                                                },
482                                            );
483                                            size.height = known_dimensions.height.unwrap_or(
484                                                match available_space.height {
485                                                    AvailableSpace::Definite(x) => x,
486                                                    AvailableSpace::MinContent
487                                                    | AvailableSpace::MaxContent => {
488                                                        size_layout.content_size.height
489                                                    }
490                                                },
491                                            );
492                                        }
493
494                                        size
495                                    }
496                                })
497                            })
498                        }
499                        ListSizingBehavior::Auto => window
500                            .with_text_style(style.text_style().cloned(), |window| {
501                                window.request_layout(style, None, cx)
502                            }),
503                    };
504
505                layout_id
506            },
507        );
508
509        (
510            layout_id,
511            VirtualListFrameState {
512                items: SmallVec::new(),
513                size_layout,
514            },
515        )
516    }
517
518    fn prepaint(
519        &mut self,
520        global_id: Option<&GlobalElementId>,
521        inspector_id: Option<&gpui::InspectorElementId>,
522        bounds: Bounds<Pixels>,
523        layout: &mut Self::RequestLayoutState,
524        window: &mut Window,
525        cx: &mut App,
526    ) -> Self::PrepaintState {
527        layout.size_layout.last_layout_bounds = bounds;
528
529        let style = self
530            .base
531            .interactivity()
532            .compute_style(global_id, None, window, cx);
533        let border_widths = style.border_widths.to_pixels(window.rem_size());
534        let paddings = style
535            .padding
536            .to_pixels(bounds.size.into(), window.rem_size());
537
538        let item_sizes = &layout.size_layout.sizes;
539        let item_origins = &layout.size_layout.origins;
540
541        let content_bounds = Bounds::from_corners(
542            bounds.origin
543                + point(
544                    border_widths.left + paddings.left,
545                    border_widths.top + paddings.top,
546                ),
547            bounds.bottom_right()
548                - point(
549                    border_widths.right + paddings.right,
550                    border_widths.bottom + paddings.bottom,
551                ),
552        );
553
554        // Update scroll_handle with the item bounds
555        let items_bounds = item_origins
556            .iter()
557            .enumerate()
558            .map(|(i, &origin)| {
559                let item_size = item_sizes[i];
560
561                Bounds {
562                    origin: match self.axis {
563                        Axis::Horizontal => point(content_bounds.left() + origin, px(0.)),
564                        Axis::Vertical => point(px(0.), content_bounds.top() + origin),
565                    },
566                    size: match self.axis {
567                        Axis::Horizontal => size(item_size, content_bounds.size.height),
568                        Axis::Vertical => size(content_bounds.size.width, item_size),
569                    },
570                }
571            })
572            .collect::<Vec<_>>();
573
574        let axis = self.axis;
575
576        let mut scroll_state = self.scroll_handle.state.borrow_mut();
577        scroll_state.axis = axis;
578        scroll_state.items_count = self.items_count;
579
580        let mut scroll_offset = self.scroll_handle.offset();
581        if let Some(scroll_to_item) = scroll_state.deferred_scroll_to_item.take() {
582            scroll_offset = self.scroll_to_deferred_item(
583                scroll_offset,
584                &items_bounds,
585                &content_bounds,
586                scroll_to_item,
587            );
588        }
589
590        scroll_offset = scroll_offset
591            .max(&point(
592                content_bounds.size.width - layout.size_layout.content_size.width,
593                content_bounds.size.height - layout.size_layout.content_size.height,
594            ))
595            .min(&point(px(0.), px(0.)));
596        if scroll_offset != self.scroll_handle.offset() {
597            self.scroll_handle.set_offset(scroll_offset);
598        }
599
600        self.base.interactivity().prepaint(
601            global_id,
602            inspector_id,
603            bounds,
604            layout.size_layout.content_size,
605            window,
606            cx,
607            |_style, _, hitbox, window, cx| {
608                if self.items_count > 0 {
609                    let min_scroll_offset = content_bounds.size.along(self.axis)
610                        - layout.size_layout.content_size.along(self.axis);
611
612                    let is_scrolled = !scroll_offset.along(self.axis).is_zero();
613                    if is_scrolled {
614                        match self.axis {
615                            Axis::Horizontal if scroll_offset.x < min_scroll_offset => {
616                                scroll_offset.x = min_scroll_offset;
617                                self.scroll_handle.set_offset(scroll_offset);
618                            }
619                            Axis::Vertical if scroll_offset.y < min_scroll_offset => {
620                                scroll_offset.y = min_scroll_offset;
621                                self.scroll_handle.set_offset(scroll_offset);
622                            }
623                            _ => {}
624                        }
625                    }
626
627                    let (first_visible_element_ix, last_visible_element_ix) = match self.axis {
628                        Axis::Horizontal => {
629                            let mut cumulative_size = px(0.);
630                            let mut first_visible_element_ix = 0;
631                            for (i, &size) in item_sizes.iter().enumerate() {
632                                cumulative_size += size;
633                                if cumulative_size > -(scroll_offset.x + paddings.left) {
634                                    first_visible_element_ix = i;
635                                    break;
636                                }
637                            }
638
639                            cumulative_size = px(0.);
640                            let mut last_visible_element_ix = 0;
641                            for (i, &size) in item_sizes.iter().enumerate() {
642                                cumulative_size += size;
643                                if cumulative_size > (-scroll_offset.x + content_bounds.size.width)
644                                {
645                                    last_visible_element_ix = i + 1;
646                                    break;
647                                }
648                            }
649                            if last_visible_element_ix == 0 {
650                                last_visible_element_ix = self.items_count;
651                            } else {
652                                last_visible_element_ix += 1;
653                            }
654                            (first_visible_element_ix, last_visible_element_ix)
655                        }
656                        Axis::Vertical => {
657                            let mut cumulative_size = px(0.);
658                            let mut first_visible_element_ix = 0;
659                            for (i, &size) in item_sizes.iter().enumerate() {
660                                cumulative_size += size;
661                                if cumulative_size > -(scroll_offset.y + paddings.top) {
662                                    first_visible_element_ix = i;
663                                    break;
664                                }
665                            }
666
667                            cumulative_size = px(0.);
668                            let mut last_visible_element_ix = 0;
669                            for (i, &size) in item_sizes.iter().enumerate() {
670                                cumulative_size += size;
671                                if cumulative_size > (-scroll_offset.y + content_bounds.size.height)
672                                {
673                                    last_visible_element_ix = i + 1;
674                                    break;
675                                }
676                            }
677                            if last_visible_element_ix == 0 {
678                                last_visible_element_ix = self.items_count;
679                            } else {
680                                last_visible_element_ix += 1;
681                            }
682                            (first_visible_element_ix, last_visible_element_ix)
683                        }
684                    };
685
686                    let visible_range = first_visible_element_ix
687                        ..cmp::min(last_visible_element_ix, self.items_count);
688
689                    let items = (self.render_items)(visible_range.clone(), window, cx);
690
691                    let content_mask = ContentMask { bounds };
692                    window.with_content_mask(Some(content_mask), |window| {
693                        for (mut item, ix) in items.into_iter().zip(visible_range.clone()) {
694                            let item_origin = match self.axis {
695                                Axis::Horizontal => {
696                                    content_bounds.origin
697                                        + point(item_origins[ix] + scroll_offset.x, scroll_offset.y)
698                                }
699                                Axis::Vertical => {
700                                    content_bounds.origin
701                                        + point(scroll_offset.x, item_origins[ix] + scroll_offset.y)
702                                }
703                            };
704
705                            let available_space = match self.axis {
706                                Axis::Horizontal => size(
707                                    AvailableSpace::Definite(item_sizes[ix]),
708                                    AvailableSpace::Definite(content_bounds.size.height),
709                                ),
710                                Axis::Vertical => size(
711                                    AvailableSpace::Definite(content_bounds.size.width),
712                                    AvailableSpace::Definite(item_sizes[ix]),
713                                ),
714                            };
715
716                            item.layout_as_root(available_space, window, cx);
717                            item.prepaint_at(item_origin, window, cx);
718                            layout.items.push(item);
719                        }
720                    });
721                }
722
723                hitbox
724            },
725        )
726    }
727
728    fn paint(
729        &mut self,
730        global_id: Option<&GlobalElementId>,
731        inspector_id: Option<&gpui::InspectorElementId>,
732        bounds: Bounds<Pixels>,
733        layout: &mut Self::RequestLayoutState,
734        hitbox: &mut Self::PrepaintState,
735        window: &mut Window,
736        cx: &mut App,
737    ) {
738        self.base.interactivity().paint(
739            global_id,
740            inspector_id,
741            bounds,
742            hitbox.as_ref(),
743            window,
744            cx,
745            |_, window, cx| {
746                for item in &mut layout.items {
747                    item.paint(window, cx);
748                }
749            },
750        )
751    }
752}