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}