Skip to main content

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