fyrox_ui/
list_view.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! List view is used to display lists with arbitrary items. It supports single-selection and by default, it stacks the items
22//! vertically.
23
24#![warn(missing_docs)]
25
26use crate::{
27    border::BorderBuilder,
28    brush::Brush,
29    core::{
30        color::Color, pool::Handle, reflect::prelude::*, type_traits::prelude::*, uuid_provider,
31        variable::InheritableVariable, visitor::prelude::*,
32    },
33    decorator::{Decorator, DecoratorMessage},
34    define_constructor,
35    draw::{CommandTexture, Draw, DrawingContext},
36    message::{KeyCode, MessageDirection, UiMessage},
37    scroll_viewer::{ScrollViewer, ScrollViewerBuilder, ScrollViewerMessage},
38    stack_panel::StackPanelBuilder,
39    style::{resource::StyleResourceExt, Style},
40    widget::{Widget, WidgetBuilder, WidgetMessage},
41    BuildContext, Control, Thickness, UiNode, UserInterface,
42};
43
44use fyrox_graph::{
45    constructor::{ConstructorProvider, GraphNodeConstructor},
46    BaseSceneGraph,
47};
48use std::ops::{Deref, DerefMut};
49
50/// A set of messages that can be used to modify/fetch the state of a [`ListView`] widget at runtime.
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub enum ListViewMessage {
53    /// A message, that is used to either fetch or modify current selection of a [`ListView`] widget.
54    SelectionChanged(Vec<usize>),
55    /// A message, that is used to set new items of a list view.
56    Items(Vec<Handle<UiNode>>),
57    /// A message, that is used to add an item to a list view.
58    AddItem(Handle<UiNode>),
59    /// A message, that is used to remove an item from a list view.
60    RemoveItem(Handle<UiNode>),
61    /// A message, that is used to bring an item into view.
62    BringItemIntoView(Handle<UiNode>),
63}
64
65impl ListViewMessage {
66    define_constructor!(
67        /// Creates [`ListViewMessage::SelectionChanged`] message.
68        ListViewMessage:SelectionChanged => fn selection(Vec<usize>), layout: false
69    );
70    define_constructor!(
71        /// Creates [`ListViewMessage::Items`] message.
72        ListViewMessage:Items => fn items(Vec<Handle<UiNode >>), layout: false
73    );
74    define_constructor!(
75        /// Creates [`ListViewMessage::AddItem`] message.
76        ListViewMessage:AddItem => fn add_item(Handle<UiNode>), layout: false
77    );
78    define_constructor!(
79        /// Creates [`ListViewMessage::RemoveItem`] message.
80        ListViewMessage:RemoveItem => fn remove_item(Handle<UiNode>), layout: false
81    );
82    define_constructor!(
83        /// Creates [`ListViewMessage::BringItemIntoView`] message.
84        ListViewMessage:BringItemIntoView => fn bring_item_into_view(Handle<UiNode>), layout: false
85    );
86}
87
88/// List view is used to display lists with arbitrary items. It supports multiple selection and by
89/// default, it stacks the items vertically (this can be changed by providing a custom panel for the
90/// items, see the section below).
91///
92/// ## Example
93///
94/// [`ListView`] can be created using [`ListViewBuilder`]:
95///
96/// ```rust
97/// # use fyrox_ui::{
98/// #     core::pool::Handle, list_view::ListViewBuilder, text::TextBuilder, widget::WidgetBuilder,
99/// #     BuildContext, UiNode,
100/// # };
101/// #
102/// fn create_list(ctx: &mut BuildContext) -> Handle<UiNode> {
103///     ListViewBuilder::new(WidgetBuilder::new())
104///         .with_items(vec![
105///             TextBuilder::new(WidgetBuilder::new())
106///                 .with_text("Item0")
107///                 .build(ctx),
108///             TextBuilder::new(WidgetBuilder::new())
109///                 .with_text("Item1")
110///                 .build(ctx),
111///         ])
112///         .build(ctx)
113/// }
114/// ```
115///
116/// Keep in mind, that the items of the list view can be pretty much any other widget. They also don't have to be the same
117/// type, you can mix any type of widgets.
118///
119/// ## Custom Items Panel
120///
121/// By default, list view creates inner [`crate::stack_panel::StackPanel`] to arrange its items. It is enough for most cases,
122/// however in rare cases you might want to use something else. For example, you could use [`crate::wrap_panel::WrapPanel`]
123/// to create list view with selectable "tiles":
124///
125/// ```rust
126/// # use fyrox_ui::{
127/// #     core::pool::Handle, list_view::ListViewBuilder, text::TextBuilder, widget::WidgetBuilder,
128/// #     wrap_panel::WrapPanelBuilder, BuildContext, UiNode,
129/// # };
130/// fn create_list(ctx: &mut BuildContext) -> Handle<UiNode> {
131///     ListViewBuilder::new(WidgetBuilder::new())
132///         // Using WrapPanel instead of StackPanel:
133///         .with_items_panel(WrapPanelBuilder::new(WidgetBuilder::new()).build(ctx))
134///         .with_items(vec![
135///             TextBuilder::new(WidgetBuilder::new())
136///                 .with_text("Item0")
137///                 .build(ctx),
138///             TextBuilder::new(WidgetBuilder::new())
139///                 .with_text("Item1")
140///                 .build(ctx),
141///         ])
142///         .build(ctx)
143/// }
144/// ```
145///
146/// ## Selection
147///
148/// List view supports any number of selected items (you can add items to the current selecting by
149/// holding Ctrl key), you can change it at runtime by sending [`ListViewMessage::SelectionChanged`]
150/// message with [`MessageDirection::ToWidget`] like so:
151///
152/// ```rust
153/// # use fyrox_ui::{
154/// #     core::pool::Handle, list_view::ListViewMessage, message::MessageDirection, UiNode,
155/// #     UserInterface,
156/// # };
157/// fn change_selection(my_list_view: Handle<UiNode>, ui: &UserInterface) {
158///     ui.send_message(ListViewMessage::selection(
159///         my_list_view,
160///         MessageDirection::ToWidget,
161///         vec![1],
162///     ));
163/// }
164/// ```
165///
166/// It is also possible to not have selected item at all, to do this you need to send an empty vector
167/// as a selection.
168///
169/// To catch the moment when selection has changed (either by a user or by the [`ListViewMessage::SelectionChanged`],) you need
170/// to listen to the same message but with opposite direction, like so:
171///
172/// ```rust
173/// # use fyrox_ui::{
174/// #     core::pool::Handle, list_view::ListViewMessage, message::MessageDirection,
175/// #     message::UiMessage, UiNode,
176/// # };
177/// #
178/// fn do_something(my_list_view: Handle<UiNode>, message: &UiMessage) {
179///     if let Some(ListViewMessage::SelectionChanged(selection)) = message.data() {
180///         if message.destination() == my_list_view
181///             && message.direction() == MessageDirection::FromWidget
182///         {
183///             println!("New selection is: {:?}", selection);
184///         }
185///     }
186/// }
187/// ```
188///
189/// ## Adding/removing items
190///
191/// To change items of the list view you can use the variety of following messages: [`ListViewMessage::AddItem`], [`ListViewMessage::RemoveItem`],
192/// [`ListViewMessage::Items`]. To decide which one to use, is very simple - if you adding/removing a few items, use [`ListViewMessage::AddItem`]
193/// and [`ListViewMessage::RemoveItem`], otherwise use [`ListViewMessage::Items`], which changes the items at once.
194///
195/// ```rust
196/// use fyrox_ui::{
197///     core::pool::Handle, list_view::ListViewMessage, message::MessageDirection,
198///     text::TextBuilder, widget::WidgetBuilder, UiNode, UserInterface,
199/// };
200/// fn change_items(my_list_view: Handle<UiNode>, ui: &mut UserInterface) {
201///     let ctx = &mut ui.build_ctx();
202///
203///     // Build new items first.
204///     let items = vec![
205///         TextBuilder::new(WidgetBuilder::new())
206///             .with_text("Item0")
207///             .build(ctx),
208///         TextBuilder::new(WidgetBuilder::new())
209///             .with_text("Item1")
210///             .build(ctx),
211///     ];
212///
213///     // Then send the message with their handles to the list view.
214///     ui.send_message(ListViewMessage::items(
215///         my_list_view,
216///         MessageDirection::ToWidget,
217///         items,
218///     ));
219/// }
220/// ```
221///
222/// ## Bringing a particular item into view
223///
224/// It is possible to bring a particular item into view, which is useful when you have hundreds or thousands of items and you
225/// want to bring only particular item into view. It could be done by sending a [`ListViewMessage::BringItemIntoView`] message:
226///
227/// ```rust
228/// # use fyrox_ui::{
229/// #     core::pool::Handle, list_view::ListViewMessage, message::MessageDirection, UiNode,
230/// #     UserInterface,
231/// # };
232/// fn bring_item_into_view(
233///     my_list_view: Handle<UiNode>,
234///     my_item: Handle<UiNode>,
235///     ui: &UserInterface,
236/// ) {
237///     ui.send_message(ListViewMessage::bring_item_into_view(
238///         my_list_view,
239///         MessageDirection::ToWidget,
240///         my_item,
241///     ));
242/// }
243/// ```
244#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
245#[visit(optional)]
246#[reflect(derived_type = "UiNode")]
247pub struct ListView {
248    /// Base widget of the list view.
249    pub widget: Widget,
250    /// Current selection.
251    pub selection: Vec<usize>,
252    /// An array of handle of item containers, which wraps the actual items.
253    pub item_containers: InheritableVariable<Vec<Handle<UiNode>>>,
254    /// Current panel widget that is used to arrange the items.
255    pub panel: InheritableVariable<Handle<UiNode>>,
256    /// Current items of the list view.
257    pub items: InheritableVariable<Vec<Handle<UiNode>>>,
258    /// Current scroll viewer instance that is used to provide scrolling functionality, when items does
259    /// not fit in the view entirely.
260    pub scroll_viewer: InheritableVariable<Handle<UiNode>>,
261}
262
263impl ConstructorProvider<UiNode, UserInterface> for ListView {
264    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
265        GraphNodeConstructor::new::<Self>()
266            .with_variant("List View", |ui| {
267                ListViewBuilder::new(WidgetBuilder::new().with_name("List View"))
268                    .build(&mut ui.build_ctx())
269                    .into()
270            })
271            .with_group("Input")
272    }
273}
274
275crate::define_widget_deref!(ListView);
276
277impl ListView {
278    /// Returns a slice with current items.
279    pub fn items(&self) -> &[Handle<UiNode>] {
280        &self.items
281    }
282
283    fn fix_selection(&self, ui: &UserInterface) {
284        // Check if current selection is out-of-bounds.
285        let mut fixed_selection = Vec::with_capacity(self.selection.len());
286
287        for &selected_index in self.selection.iter() {
288            if selected_index >= self.items.len() {
289                if !self.items.is_empty() {
290                    fixed_selection.push(self.items.len() - 1);
291                }
292            } else {
293                fixed_selection.push(selected_index);
294            }
295        }
296
297        if self.selection != fixed_selection {
298            ui.send_message(ListViewMessage::selection(
299                self.handle,
300                MessageDirection::ToWidget,
301                fixed_selection,
302            ));
303        }
304    }
305
306    fn largest_selection_index(&self) -> Option<usize> {
307        self.selection.iter().max().cloned()
308    }
309
310    fn smallest_selection_index(&self) -> Option<usize> {
311        self.selection.iter().min().cloned()
312    }
313
314    fn sync_decorators(&self, ui: &UserInterface) {
315        for (i, &container) in self.item_containers.iter().enumerate() {
316            let select = self.selection.contains(&i);
317            if let Some(container) = ui.node(container).cast::<ListViewItem>() {
318                let mut stack = container.children().to_vec();
319                while let Some(handle) = stack.pop() {
320                    let node = ui.node(handle);
321
322                    if node.cast::<ListView>().is_some() {
323                        // Do nothing.
324                    } else if node.cast::<Decorator>().is_some() {
325                        ui.send_message(DecoratorMessage::select(
326                            handle,
327                            MessageDirection::ToWidget,
328                            select,
329                        ));
330                    } else {
331                        stack.extend_from_slice(node.children())
332                    }
333                }
334            }
335        }
336    }
337}
338
339/// A wrapper for list view items, that is used to add selection functionality to arbitrary items.
340#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
341#[reflect(derived_type = "UiNode")]
342pub struct ListViewItem {
343    /// Base widget of the list view item.
344    pub widget: Widget,
345}
346
347impl ConstructorProvider<UiNode, UserInterface> for ListViewItem {
348    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
349        GraphNodeConstructor::new::<Self>()
350            .with_variant("List View Item", |ui: &mut UserInterface| {
351                let node = UiNode::new(ListViewItem {
352                    widget: WidgetBuilder::new()
353                        .with_name("List View Item")
354                        .build(&ui.build_ctx()),
355                });
356                ui.add_node(node).into()
357            })
358            .with_group("Input")
359    }
360}
361
362crate::define_widget_deref!(ListViewItem);
363
364uuid_provider!(ListViewItem = "02f21415-5843-42f5-a3e4-b4a21e7739ad");
365
366impl Control for ListViewItem {
367    fn draw(&self, drawing_context: &mut DrawingContext) {
368        // Emit transparent geometry so item container can be picked by hit test.
369        drawing_context.push_rect_filled(&self.widget.bounding_rect(), None);
370        drawing_context.commit(
371            self.clip_bounds(),
372            Brush::Solid(Color::TRANSPARENT),
373            CommandTexture::None,
374            &self.material,
375            None,
376        );
377    }
378
379    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
380        self.widget.handle_routed_message(ui, message);
381
382        let parent_list_view =
383            self.find_by_criteria_up(ui, |node| node.cast::<ListView>().is_some());
384
385        if let Some(WidgetMessage::MouseUp { .. }) = message.data::<WidgetMessage>() {
386            if !message.handled() {
387                let list_view = ui
388                    .node(parent_list_view)
389                    .cast::<ListView>()
390                    .expect("Parent of ListViewItem must be ListView!");
391
392                let self_index = list_view
393                    .item_containers
394                    .iter()
395                    .position(|c| *c == self.handle)
396                    .expect("ListViewItem must be used as a child of ListView");
397
398                let new_selection = if ui.keyboard_modifiers.control {
399                    let mut selection = list_view.selection.clone();
400                    selection.push(self_index);
401                    selection
402                } else {
403                    vec![self_index]
404                };
405
406                // Explicitly set selection on parent items control. This will send
407                // SelectionChanged message and all items will react.
408                ui.send_message(ListViewMessage::selection(
409                    parent_list_view,
410                    MessageDirection::ToWidget,
411                    new_selection,
412                ));
413                message.set_handled(true);
414            }
415        }
416    }
417}
418
419uuid_provider!(ListView = "5832a643-5bf9-4d84-8358-b4c45bb440e8");
420
421impl Control for ListView {
422    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
423        self.widget.handle_routed_message(ui, message);
424
425        if let Some(msg) = message.data::<ListViewMessage>() {
426            if message.destination() == self.handle()
427                && message.direction() == MessageDirection::ToWidget
428            {
429                match msg {
430                    ListViewMessage::Items(items) => {
431                        // Generate new items.
432                        let item_containers = generate_item_containers(&mut ui.build_ctx(), items);
433
434                        ui.send_message(WidgetMessage::replace_children(
435                            *self.panel,
436                            MessageDirection::ToWidget,
437                            item_containers.clone(),
438                        ));
439
440                        self.item_containers
441                            .set_value_and_mark_modified(item_containers);
442                        self.items.set_value_and_mark_modified(items.clone());
443
444                        self.fix_selection(ui);
445                        self.sync_decorators(ui);
446                    }
447                    &ListViewMessage::AddItem(item) => {
448                        let item_container = generate_item_container(&mut ui.build_ctx(), item);
449
450                        ui.send_message(WidgetMessage::link(
451                            item_container,
452                            MessageDirection::ToWidget,
453                            *self.panel,
454                        ));
455
456                        self.item_containers.push(item_container);
457                        self.items.push(item);
458                    }
459                    ListViewMessage::SelectionChanged(selection) => {
460                        if &self.selection != selection {
461                            self.selection.clone_from(selection);
462                            self.sync_decorators(ui);
463                            ui.send_message(message.reverse());
464                        }
465                    }
466                    &ListViewMessage::RemoveItem(item) => {
467                        if let Some(item_position) = self.items.iter().position(|i| *i == item) {
468                            self.items.remove(item_position);
469                            self.item_containers.remove(item_position);
470
471                            let container = ui.node(item).parent();
472
473                            ui.send_message(WidgetMessage::remove(
474                                container,
475                                MessageDirection::ToWidget,
476                            ));
477
478                            self.fix_selection(ui);
479                            self.sync_decorators(ui);
480                        }
481                    }
482                    &ListViewMessage::BringItemIntoView(item) => {
483                        if self.items.contains(&item) {
484                            ui.send_message(ScrollViewerMessage::bring_into_view(
485                                *self.scroll_viewer,
486                                MessageDirection::ToWidget,
487                                item,
488                            ));
489                        }
490                    }
491                }
492            }
493        } else if let Some(WidgetMessage::KeyDown(key_code)) = message.data() {
494            if !message.handled() {
495                let new_selection = if *key_code == KeyCode::ArrowDown {
496                    match self.largest_selection_index() {
497                        Some(i) => Some(i.saturating_add(1) % self.items.len()),
498                        None => {
499                            if self.items.is_empty() {
500                                None
501                            } else {
502                                Some(0)
503                            }
504                        }
505                    }
506                } else if *key_code == KeyCode::ArrowUp {
507                    match self.smallest_selection_index() {
508                        Some(i) => {
509                            let mut index = (i as isize).saturating_sub(1);
510                            let count = self.items.len() as isize;
511                            if index < 0 {
512                                index += count;
513                            }
514                            Some((index % count) as usize)
515                        }
516                        None => {
517                            if self.items.is_empty() {
518                                None
519                            } else {
520                                Some(0)
521                            }
522                        }
523                    }
524                } else {
525                    None
526                };
527
528                if let Some(new_selection) = new_selection {
529                    ui.send_message(ListViewMessage::selection(
530                        self.handle,
531                        MessageDirection::ToWidget,
532                        vec![new_selection],
533                    ));
534
535                    ui.send_message(ListViewMessage::bring_item_into_view(
536                        self.handle,
537                        MessageDirection::ToWidget,
538                        self.items[new_selection],
539                    ));
540
541                    message.set_handled(true);
542                }
543            }
544        }
545    }
546}
547
548/// List view builder is used to create [`ListView`] widget instances and add them to a user interface.
549pub struct ListViewBuilder {
550    widget_builder: WidgetBuilder,
551    items: Vec<Handle<UiNode>>,
552    panel: Option<Handle<UiNode>>,
553    scroll_viewer: Option<Handle<UiNode>>,
554    selection: Vec<usize>,
555}
556
557impl ListViewBuilder {
558    /// Creates new list view builder.
559    pub fn new(widget_builder: WidgetBuilder) -> Self {
560        Self {
561            widget_builder,
562            items: Vec::new(),
563            panel: None,
564            scroll_viewer: None,
565            selection: Default::default(),
566        }
567    }
568
569    /// Sets an array of handle of desired items for the list view.
570    pub fn with_items(mut self, items: Vec<Handle<UiNode>>) -> Self {
571        self.items = items;
572        self
573    }
574
575    /// Sets the desired item panel that will be used to arrange the items.
576    pub fn with_items_panel(mut self, panel: Handle<UiNode>) -> Self {
577        self.panel = Some(panel);
578        self
579    }
580
581    /// Sets the desired scroll viewer.
582    pub fn with_scroll_viewer(mut self, sv: Handle<UiNode>) -> Self {
583        self.scroll_viewer = Some(sv);
584        self
585    }
586
587    /// Sets the desired selected items.
588    pub fn with_selection(mut self, items: Vec<usize>) -> Self {
589        self.selection = items;
590        self
591    }
592
593    /// Finishes list view building and adds it to the user interface.
594    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
595        let item_containers = generate_item_containers(ctx, &self.items);
596
597        // Sync the decorators to the actual state of items.
598        for (i, &container) in item_containers.iter().enumerate() {
599            let select = self.selection.contains(&i);
600            if let Some(container) = ctx[container].cast::<ListViewItem>() {
601                let mut stack = container.children().to_vec();
602                while let Some(handle) = stack.pop() {
603                    let node = &mut ctx[handle];
604                    if node.cast::<ListView>().is_some() {
605                        // Do nothing.
606                    } else if let Some(decorator) = node.cast_mut::<Decorator>() {
607                        decorator.is_selected.set_value_and_mark_modified(select);
608                        if select {
609                            decorator.background = (*decorator.selected_brush).clone().into();
610                        } else {
611                            decorator.background = (*decorator.normal_brush).clone().into();
612                        }
613                    } else {
614                        stack.extend_from_slice(node.children())
615                    }
616                }
617            }
618        }
619
620        let panel = self
621            .panel
622            .unwrap_or_else(|| StackPanelBuilder::new(WidgetBuilder::new()).build(ctx));
623
624        for &item_container in item_containers.iter() {
625            ctx.link(item_container, panel);
626        }
627
628        let style = &ctx.style;
629        let back = BorderBuilder::new(
630            WidgetBuilder::new()
631                .with_background(style.property(Style::BRUSH_DARK))
632                .with_foreground(style.property(Style::BRUSH_LIGHT)),
633        )
634        .with_stroke_thickness(Thickness::uniform(1.0).into())
635        .build(ctx);
636
637        let scroll_viewer = self.scroll_viewer.unwrap_or_else(|| {
638            ScrollViewerBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(0.0)))
639                .build(ctx)
640        });
641        let scroll_viewer_ref = ctx[scroll_viewer]
642            .cast_mut::<ScrollViewer>()
643            .expect("ListView must have ScrollViewer");
644        scroll_viewer_ref.content = panel;
645        let content_presenter = scroll_viewer_ref.scroll_panel;
646        ctx.link(panel, content_presenter);
647
648        ctx.link(scroll_viewer, back);
649
650        let list_box = ListView {
651            widget: self
652                .widget_builder
653                .with_accepts_input(true)
654                .with_child(back)
655                .build(ctx),
656            selection: self.selection,
657            item_containers: item_containers.into(),
658            items: self.items.into(),
659            panel: panel.into(),
660            scroll_viewer: scroll_viewer.into(),
661        };
662
663        ctx.add_node(UiNode::new(list_box))
664    }
665}
666
667fn generate_item_container(ctx: &mut BuildContext, item: Handle<UiNode>) -> Handle<UiNode> {
668    let item = ListViewItem {
669        widget: WidgetBuilder::new().with_child(item).build(ctx),
670    };
671
672    ctx.add_node(UiNode::new(item))
673}
674
675fn generate_item_containers(
676    ctx: &mut BuildContext,
677    items: &[Handle<UiNode>],
678) -> Vec<Handle<UiNode>> {
679    items
680        .iter()
681        .map(|&item| generate_item_container(ctx, item))
682        .collect()
683}
684
685#[cfg(test)]
686mod test {
687    use crate::list_view::ListViewBuilder;
688    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
689
690    #[test]
691    fn test_deletion() {
692        test_widget_deletion(|ctx| ListViewBuilder::new(WidgetBuilder::new()).build(ctx));
693    }
694}