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