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