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