Skip to main content

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