fyrox_ui/
dropdown_list.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//! Drop-down list. This is control which shows currently selected item and provides drop-down
22//! list to select its current item. It is build using composition with standard list view.
23//! See [`DropdownList`] docs for more info and usage examples.
24
25#![warn(missing_docs)]
26
27use crate::{
28    border::BorderBuilder,
29    core::{
30        algebra::Vector2, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
31        uuid_provider, variable::InheritableVariable, visitor::prelude::*,
32    },
33    define_constructor,
34    grid::{Column, GridBuilder, Row},
35    list_view::{ListViewBuilder, ListViewMessage},
36    message::{KeyCode, MessageDirection, UiMessage},
37    popup::{Placement, PopupBuilder, PopupMessage},
38    style::{resource::StyleResourceExt, Style},
39    utils::{make_arrow_non_uniform_size, ArrowDirection},
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::{
49    ops::{Deref, DerefMut},
50    sync::mpsc::Sender,
51};
52
53/// A set of possible messages for [`DropdownList`] widget.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum DropdownListMessage {
56    /// A message, that is used to set new selection and receive selection changes.
57    SelectionChanged(Option<usize>),
58    /// A message, that is used to set new items of a dropdown list.
59    Items(Vec<Handle<UiNode>>),
60    /// A message, that is used to add an item to a dropdown list.
61    AddItem(Handle<UiNode>),
62    /// A message, that is used to open a dropdown list.
63    Open,
64    /// A message, that is used to close a dropdown list.
65    Close,
66}
67
68impl DropdownListMessage {
69    define_constructor!(
70        /// Creates [`DropdownListMessage::SelectionChanged`] message.
71        DropdownListMessage:SelectionChanged => fn selection(Option<usize>), layout: false
72    );
73    define_constructor!(
74           /// Creates [`DropdownListMessage::Items`] message.
75        DropdownListMessage:Items => fn items(Vec<Handle<UiNode >>), layout: false
76    );
77    define_constructor!(
78        /// Creates [`DropdownListMessage::AddItem`] message.
79        DropdownListMessage:AddItem => fn add_item(Handle<UiNode>), layout: false
80    );
81    define_constructor!(
82        /// Creates [`DropdownListMessage::Open`] message.
83        DropdownListMessage:Open => fn open(), layout: false
84    );
85    define_constructor!(
86        /// Creates [`DropdownListMessage::Close`] message.
87        DropdownListMessage:Close => fn close(), layout: false
88    );
89}
90
91/// Drop-down list is a control which shows currently selected item and provides drop-down
92/// list to select its current item. It is used to show a single selected item in compact way.
93///
94/// ## Example
95///
96/// A dropdown list with two text items with the last one selected, could be created like so:
97///
98/// ```rust
99/// # use fyrox_ui::{
100/// #     core::pool::Handle, dropdown_list::DropdownListBuilder, text::TextBuilder,
101/// #     widget::WidgetBuilder, BuildContext, UiNode,
102/// # };
103/// #
104/// fn create_drop_down_list(ctx: &mut BuildContext) -> Handle<UiNode> {
105///     DropdownListBuilder::new(WidgetBuilder::new())
106///         .with_items(vec![
107///             TextBuilder::new(WidgetBuilder::new())
108///                 .with_text("Item 0")
109///                 .build(ctx),
110///             TextBuilder::new(WidgetBuilder::new())
111///                 .with_text("Item 1")
112///                 .build(ctx),
113///         ])
114///         .with_selected(1)
115///         .build(ctx)
116/// }
117/// ```
118///
119/// Keep in mind, that items of a dropdown list could be any widget, but usually each item is wrapped
120/// in some other widget that shows current state of items (selected, hovered, clicked, etc.). One
121/// of the most convenient way of doing this is to use Decorator widget:
122///
123/// ```rust
124/// # use fyrox_ui::{
125/// #     border::BorderBuilder, core::pool::Handle, decorator::DecoratorBuilder,
126/// #     dropdown_list::DropdownListBuilder, text::TextBuilder, widget::WidgetBuilder, BuildContext,
127/// #     UiNode,
128/// # };
129/// #
130/// fn make_item(text: &str, ctx: &mut BuildContext) -> Handle<UiNode> {
131///     DecoratorBuilder::new(BorderBuilder::new(
132///         WidgetBuilder::new().with_child(
133///             TextBuilder::new(WidgetBuilder::new())
134///                 .with_text(text)
135///                 .build(ctx),
136///         ),
137///     ))
138///     .build(ctx)
139/// }
140///
141/// fn create_drop_down_list_with_decorators(ctx: &mut BuildContext) -> Handle<UiNode> {
142///     DropdownListBuilder::new(WidgetBuilder::new())
143///         .with_items(vec![make_item("Item 0", ctx), make_item("Item 1", ctx)])
144///         .with_selected(1)
145///         .build(ctx)
146/// }
147/// ```
148///
149/// ## Selection
150///
151/// Dropdown list supports two kinds of selection - `None` or `Some(index)`. To catch a moment when
152/// selection changes, use the following code:
153///
154/// ```rust
155/// use fyrox_ui::{
156///     core::pool::Handle,
157///     dropdown_list::DropdownListMessage,
158///     message::{MessageDirection, UiMessage},
159///     UiNode,
160/// };
161///
162/// struct Foo {
163///     dropdown_list: Handle<UiNode>,
164/// }
165///
166/// impl Foo {
167///     fn on_ui_message(&mut self, message: &UiMessage) {
168///         if let Some(DropdownListMessage::SelectionChanged(new_selection)) = message.data() {
169///             if message.destination() == self.dropdown_list
170///                 && message.direction() == MessageDirection::FromWidget
171///             {
172///                 // Do something.
173///                 dbg!(new_selection);
174///             }
175///         }
176///     }
177/// }
178/// ```
179///
180/// To change selection of a dropdown list, send [`DropdownListMessage::SelectionChanged`] message
181/// to it.
182///
183/// ## Items
184///
185/// To change current items of a dropdown list, create the items first and then send them to the
186/// dropdown list using [`DropdownListMessage::Items`] message.
187///
188/// ## Opening and Closing
189///
190/// A dropdown list could be opened and closed manually using [`DropdownListMessage::Open`] and
191/// [`DropdownListMessage::Close`] messages.  
192#[derive(Default, Clone, Debug, Visit, Reflect, ComponentProvider)]
193#[reflect(derived_type = "UiNode")]
194pub struct DropdownList {
195    /// Base widget of the dropdown list.
196    pub widget: Widget,
197    /// A handle of the inner popup of the dropdown list. It holds the actual items of the list.
198    pub popup: InheritableVariable<Handle<UiNode>>,
199    /// A list of handles of items of the dropdown list.
200    pub items: InheritableVariable<Vec<Handle<UiNode>>>,
201    /// A handle to the `ListView` widget, that holds the items of the dropdown list.
202    pub list_view: InheritableVariable<Handle<UiNode>>,
203    /// A handle to a currently selected item.
204    pub current: InheritableVariable<Handle<UiNode>>,
205    /// An index of currently selected item (or [`None`] if there's nothing selected).
206    pub selection: InheritableVariable<Option<usize>>,
207    /// A flag, that defines whether the dropdown list's popup should close after selection or not.
208    pub close_on_selection: InheritableVariable<bool>,
209    /// A handle to an inner Grid widget, that holds currently selected item and other decorators.
210    pub main_grid: InheritableVariable<Handle<UiNode>>,
211}
212
213impl ConstructorProvider<UiNode, UserInterface> for DropdownList {
214    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
215        GraphNodeConstructor::new::<Self>()
216            .with_variant("Dropdown List", |ui| {
217                DropdownListBuilder::new(WidgetBuilder::new().with_name("Dropdown List"))
218                    .build(&mut ui.build_ctx())
219                    .into()
220            })
221            .with_group("Input")
222    }
223}
224
225crate::define_widget_deref!(DropdownList);
226
227uuid_provider!(DropdownList = "1da2f69a-c8b4-4ae2-a2ad-4afe61ee2a32");
228
229impl Control for DropdownList {
230    fn on_remove(&self, sender: &Sender<UiMessage>) {
231        // Popup won't be deleted with the dropdown list, because it is not the child of the list.
232        // So we have to remove it manually.
233        sender
234            .send(WidgetMessage::remove(
235                *self.popup,
236                MessageDirection::ToWidget,
237            ))
238            .unwrap();
239    }
240
241    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
242        self.widget.handle_routed_message(ui, message);
243
244        if let Some(msg) = message.data::<WidgetMessage>() {
245            match msg {
246                WidgetMessage::MouseDown { .. } => {
247                    if message.destination() == self.handle()
248                        || self.widget.has_descendant(message.destination(), ui)
249                    {
250                        ui.send_message(DropdownListMessage::open(
251                            self.handle,
252                            MessageDirection::ToWidget,
253                        ));
254                    }
255                }
256                WidgetMessage::KeyDown(key_code) => {
257                    if !message.handled() {
258                        if *key_code == KeyCode::ArrowDown {
259                            ui.send_message(DropdownListMessage::open(
260                                self.handle,
261                                MessageDirection::ToWidget,
262                            ));
263                        } else if *key_code == KeyCode::ArrowUp {
264                            ui.send_message(DropdownListMessage::close(
265                                self.handle,
266                                MessageDirection::ToWidget,
267                            ));
268                        }
269                        message.set_handled(true);
270                    }
271                }
272                _ => (),
273            }
274        } else if let Some(msg) = message.data::<DropdownListMessage>() {
275            if message.destination() == self.handle()
276                && message.direction() == MessageDirection::ToWidget
277            {
278                match msg {
279                    DropdownListMessage::Open => {
280                        ui.send_message(WidgetMessage::width(
281                            *self.popup,
282                            MessageDirection::ToWidget,
283                            self.actual_local_size().x,
284                        ));
285                        ui.send_message(PopupMessage::placement(
286                            *self.popup,
287                            MessageDirection::ToWidget,
288                            Placement::LeftBottom(self.handle),
289                        ));
290                        ui.send_message(PopupMessage::open(
291                            *self.popup,
292                            MessageDirection::ToWidget,
293                        ));
294                    }
295                    DropdownListMessage::Close => {
296                        ui.send_message(PopupMessage::close(
297                            *self.popup,
298                            MessageDirection::ToWidget,
299                        ));
300                    }
301                    DropdownListMessage::Items(items) => {
302                        ui.send_message(ListViewMessage::items(
303                            *self.list_view,
304                            MessageDirection::ToWidget,
305                            items.clone(),
306                        ));
307                        self.items.set_value_and_mark_modified(items.clone());
308                        self.sync_selected_item_preview(ui);
309                    }
310                    &DropdownListMessage::AddItem(item) => {
311                        ui.send_message(ListViewMessage::add_item(
312                            *self.list_view,
313                            MessageDirection::ToWidget,
314                            item,
315                        ));
316                        self.items.push(item);
317                    }
318                    &DropdownListMessage::SelectionChanged(selection) => {
319                        if selection != *self.selection {
320                            self.selection.set_value_and_mark_modified(selection);
321                            ui.send_message(ListViewMessage::selection(
322                                *self.list_view,
323                                MessageDirection::ToWidget,
324                                selection.map(|index| vec![index]).unwrap_or_default(),
325                            ));
326
327                            self.sync_selected_item_preview(ui);
328
329                            if *self.close_on_selection {
330                                ui.send_message(PopupMessage::close(
331                                    *self.popup,
332                                    MessageDirection::ToWidget,
333                                ));
334                            }
335
336                            ui.send_message(message.reverse());
337                        }
338                    }
339                }
340            }
341        }
342    }
343
344    fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
345        if let Some(ListViewMessage::SelectionChanged(selection)) =
346            message.data::<ListViewMessage>()
347        {
348            let selection = selection.first().cloned();
349            if message.direction() == MessageDirection::FromWidget
350                && message.destination() == *self.list_view
351                && *self.selection != selection
352            {
353                // Post message again but from name of this drop-down list so user can catch
354                // message and respond properly.
355                ui.send_message(DropdownListMessage::selection(
356                    self.handle,
357                    MessageDirection::ToWidget,
358                    selection,
359                ));
360            }
361        } else if let Some(msg) = message.data::<PopupMessage>() {
362            if message.destination() == *self.popup
363                && message.direction() == MessageDirection::ToWidget
364            {
365                match msg {
366                    PopupMessage::Open => {
367                        ui.send_message(DropdownListMessage::open(
368                            self.handle,
369                            MessageDirection::FromWidget,
370                        ));
371                    }
372                    PopupMessage::Close => {
373                        ui.send_message(DropdownListMessage::close(
374                            self.handle,
375                            MessageDirection::FromWidget,
376                        ));
377
378                        ui.send_message(WidgetMessage::focus(
379                            self.handle,
380                            MessageDirection::ToWidget,
381                        ));
382                    }
383                    _ => (),
384                }
385            }
386        }
387    }
388}
389
390impl DropdownList {
391    /// A name of style property, that defines corner radius of a dropdown list.
392    pub const CORNER_RADIUS: &'static str = "DropdownList.CornerRadius";
393
394    /// Returns a style of the widget. This style contains only widget-specific properties.
395    pub fn style() -> Style {
396        Style::default().with(Self::CORNER_RADIUS, 4.0f32)
397    }
398
399    fn sync_selected_item_preview(&mut self, ui: &mut UserInterface) {
400        // Copy node from current selection in list view. This is not
401        // always suitable because if an item has some visual behaviour
402        // (change color on mouse hover, change something on click, etc)
403        // it will be also reflected in selected item.
404        if self.current.is_some() {
405            ui.send_message(WidgetMessage::remove(
406                *self.current,
407                MessageDirection::ToWidget,
408            ));
409        }
410        if let Some(index) = *self.selection {
411            if let Some(item) = self.items.get(index) {
412                self.current
413                    .set_value_and_mark_modified(ui.copy_node(*item));
414                ui.send_message(WidgetMessage::link(
415                    *self.current,
416                    MessageDirection::ToWidget,
417                    *self.main_grid,
418                ));
419                ui.node(*self.current).request_update_visibility();
420                ui.send_message(WidgetMessage::margin(
421                    *self.current,
422                    MessageDirection::ToWidget,
423                    Thickness::uniform(0.0),
424                ));
425                ui.send_message(WidgetMessage::reset_visual(
426                    *self.current,
427                    MessageDirection::ToWidget,
428                ));
429            } else {
430                self.current.set_value_and_mark_modified(Handle::NONE);
431            }
432        } else {
433            self.current.set_value_and_mark_modified(Handle::NONE);
434        }
435    }
436}
437
438/// Dropdown list builder allows to create [`DropdownList`] widgets and add them a user interface.
439pub struct DropdownListBuilder {
440    widget_builder: WidgetBuilder,
441    items: Vec<Handle<UiNode>>,
442    selected: Option<usize>,
443    close_on_selection: bool,
444}
445
446impl DropdownListBuilder {
447    /// Creates new dropdown list builder.
448    pub fn new(widget_builder: WidgetBuilder) -> Self {
449        Self {
450            widget_builder,
451            items: Default::default(),
452            selected: None,
453            close_on_selection: false,
454        }
455    }
456
457    /// Sets the desired items of the dropdown list.
458    pub fn with_items(mut self, items: Vec<Handle<UiNode>>) -> Self {
459        self.items = items;
460        self
461    }
462
463    /// Sets the selected item of the dropdown list.
464    pub fn with_selected(mut self, index: usize) -> Self {
465        self.selected = Some(index);
466        self
467    }
468
469    /// Sets the desired items of the dropdown list.
470    pub fn with_opt_selected(mut self, index: Option<usize>) -> Self {
471        self.selected = index;
472        self
473    }
474
475    /// Sets a flag, that defines whether the dropdown list should close on selection or not.
476    pub fn with_close_on_selection(mut self, value: bool) -> Self {
477        self.close_on_selection = value;
478        self
479    }
480
481    /// Finishes list building and adds it to the given user interface.
482    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode>
483    where
484        Self: Sized,
485    {
486        let items_control = ListViewBuilder::new(
487            WidgetBuilder::new().with_max_size(Vector2::new(f32::INFINITY, 200.0)),
488        )
489        .with_items(self.items.clone())
490        .build(ctx);
491
492        let popup = PopupBuilder::new(WidgetBuilder::new())
493            .with_content(items_control)
494            .build(ctx);
495
496        let current = if let Some(selected) = self.selected {
497            self.items
498                .get(selected)
499                .map_or(Handle::NONE, |&f| ctx.copy(f))
500        } else {
501            Handle::NONE
502        };
503
504        let arrow = make_arrow_non_uniform_size(ctx, ArrowDirection::Bottom, 10.0, 5.0);
505        ctx[arrow].set_margin(Thickness::left_right(2.0));
506        ctx[arrow].set_column(1);
507
508        let main_grid =
509            GridBuilder::new(WidgetBuilder::new().with_child(current).with_child(arrow))
510                .add_row(Row::stretch())
511                .add_column(Column::stretch())
512                .add_column(Column::auto())
513                .build(ctx);
514
515        let border = BorderBuilder::new(
516            WidgetBuilder::new()
517                .with_background(ctx.style.property(Style::BRUSH_DARKER))
518                .with_foreground(ctx.style.property(Style::BRUSH_LIGHT))
519                .with_child(main_grid),
520        )
521        .with_pad_by_corner_radius(false)
522        .with_corner_radius(ctx.style.property(DropdownList::CORNER_RADIUS))
523        .build(ctx);
524
525        let dropdown_list = UiNode::new(DropdownList {
526            widget: self
527                .widget_builder
528                .with_accepts_input(true)
529                .with_preview_messages(true)
530                .with_child(border)
531                .build(ctx),
532            popup: popup.into(),
533            items: self.items.into(),
534            list_view: items_control.into(),
535            current: current.into(),
536            selection: self.selected.into(),
537            close_on_selection: self.close_on_selection.into(),
538            main_grid: main_grid.into(),
539        });
540
541        ctx.add_node(dropdown_list)
542    }
543}
544
545#[cfg(test)]
546mod test {
547    use crate::dropdown_list::DropdownListBuilder;
548    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
549
550    #[test]
551    fn test_deletion() {
552        test_widget_deletion(|ctx| DropdownListBuilder::new(WidgetBuilder::new()).build(ctx));
553    }
554}