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}