rg3d_ui/
dropdown_list.rs

1//! Drop-down list. This is control which shows currently selected item and provides drop-down
2//! list to select its current item. It is build using composition with standard list view.
3
4use crate::{
5    border::BorderBuilder,
6    core::{algebra::Vector2, pool::Handle},
7    define_constructor,
8    grid::{Column, GridBuilder, Row},
9    list_view::{ListViewBuilder, ListViewMessage},
10    message::{MessageDirection, UiMessage},
11    popup::{Placement, PopupBuilder, PopupMessage},
12    utils::{make_arrow, ArrowDirection},
13    widget::Widget,
14    widget::{WidgetBuilder, WidgetMessage},
15    BuildContext, Control, NodeHandleMapping, UiNode, UserInterface, BRUSH_LIGHT,
16};
17use std::sync::mpsc::Sender;
18use std::{
19    any::{Any, TypeId},
20    ops::{Deref, DerefMut},
21};
22
23#[derive(Debug, Clone, PartialEq)]
24pub enum DropdownListMessage {
25    SelectionChanged(Option<usize>),
26    Items(Vec<Handle<UiNode>>),
27    AddItem(Handle<UiNode>),
28    Open,
29    Close,
30}
31
32impl DropdownListMessage {
33    define_constructor!(DropdownListMessage:SelectionChanged => fn selection(Option<usize>), layout: false);
34    define_constructor!(DropdownListMessage:Items => fn items(Vec<Handle<UiNode >>), layout: false);
35    define_constructor!(DropdownListMessage:AddItem => fn add_item(Handle<UiNode>), layout: false);
36    define_constructor!(DropdownListMessage:Open => fn open(), layout: false);
37    define_constructor!(DropdownListMessage:Close => fn close(), layout: false);
38}
39
40#[derive(Clone)]
41pub struct DropdownList {
42    widget: Widget,
43    popup: Handle<UiNode>,
44    items: Vec<Handle<UiNode>>,
45    list_view: Handle<UiNode>,
46    current: Handle<UiNode>,
47    selection: Option<usize>,
48    close_on_selection: bool,
49    main_grid: Handle<UiNode>,
50}
51
52crate::define_widget_deref!(DropdownList);
53
54impl Control for DropdownList {
55    fn query_component(&self, type_id: TypeId) -> Option<&dyn Any> {
56        if type_id == TypeId::of::<Self>() {
57            Some(self)
58        } else {
59            None
60        }
61    }
62
63    fn on_remove(&self, sender: &Sender<UiMessage>) {
64        // Popup won't be deleted with the dropdown list, because it is not the child of the list.
65        // So we have to remove it manually.
66        sender
67            .send(WidgetMessage::remove(
68                self.popup,
69                MessageDirection::ToWidget,
70            ))
71            .unwrap();
72    }
73
74    fn resolve(&mut self, node_map: &NodeHandleMapping) {
75        node_map.resolve(&mut self.popup);
76        node_map.resolve(&mut self.list_view);
77        node_map.resolve(&mut self.current);
78        node_map.resolve(&mut self.main_grid);
79        node_map.resolve_slice(&mut self.items);
80    }
81
82    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
83        self.widget.handle_routed_message(ui, message);
84
85        if let Some(WidgetMessage::MouseDown { .. }) = message.data::<WidgetMessage>() {
86            if message.destination() == self.handle()
87                || self.widget.has_descendant(message.destination(), ui)
88            {
89                ui.send_message(DropdownListMessage::open(
90                    self.handle,
91                    MessageDirection::ToWidget,
92                ));
93            }
94        } else if let Some(msg) = message.data::<DropdownListMessage>() {
95            if message.destination() == self.handle()
96                && message.direction() == MessageDirection::ToWidget
97            {
98                match msg {
99                    DropdownListMessage::Open => {
100                        ui.send_message(WidgetMessage::width(
101                            self.popup,
102                            MessageDirection::ToWidget,
103                            self.actual_size().x,
104                        ));
105                        ui.send_message(PopupMessage::placement(
106                            self.popup,
107                            MessageDirection::ToWidget,
108                            Placement::LeftBottom(self.handle),
109                        ));
110                        ui.send_message(PopupMessage::open(self.popup, MessageDirection::ToWidget));
111                    }
112                    DropdownListMessage::Close => {
113                        ui.send_message(PopupMessage::close(
114                            self.popup,
115                            MessageDirection::ToWidget,
116                        ));
117                    }
118                    DropdownListMessage::Items(items) => {
119                        ui.send_message(ListViewMessage::items(
120                            self.list_view,
121                            MessageDirection::ToWidget,
122                            items.clone(),
123                        ));
124                        self.items = items.clone();
125                    }
126                    &DropdownListMessage::AddItem(item) => {
127                        ListViewMessage::add_item(self.list_view, MessageDirection::ToWidget, item);
128                        self.items.push(item);
129                    }
130                    &DropdownListMessage::SelectionChanged(selection) => {
131                        if selection != self.selection {
132                            self.selection = selection;
133                            ui.send_message(ListViewMessage::selection(
134                                self.list_view,
135                                MessageDirection::ToWidget,
136                                selection,
137                            ));
138
139                            // Copy node from current selection in list view. This is not
140                            // always suitable because if an item has some visual behaviour
141                            // (change color on mouse hover, change something on click, etc)
142                            // it will be also reflected in selected item.
143                            if self.current.is_some() {
144                                ui.send_message(WidgetMessage::remove(
145                                    self.current,
146                                    MessageDirection::ToWidget,
147                                ));
148                            }
149                            if let Some(index) = selection {
150                                if let Some(item) = self.items.get(index) {
151                                    self.current = ui.copy_node(*item);
152                                    ui.send_message(WidgetMessage::link(
153                                        self.current,
154                                        MessageDirection::ToWidget,
155                                        self.main_grid,
156                                    ));
157                                } else {
158                                    self.current = Handle::NONE;
159                                }
160                            } else {
161                                self.current = Handle::NONE;
162                            }
163
164                            if self.close_on_selection {
165                                ui.send_message(PopupMessage::close(
166                                    self.popup,
167                                    MessageDirection::ToWidget,
168                                ));
169                            }
170
171                            ui.send_message(message.reverse());
172                        }
173                    }
174                }
175            }
176        }
177    }
178
179    fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
180        if let Some(ListViewMessage::SelectionChanged(selection)) =
181            message.data::<ListViewMessage>()
182        {
183            if message.direction() == MessageDirection::FromWidget
184                && message.destination() == self.list_view
185                && &self.selection != selection
186            {
187                // Post message again but from name of this drop-down list so user can catch
188                // message and respond properly.
189                ui.send_message(DropdownListMessage::selection(
190                    self.handle,
191                    MessageDirection::ToWidget,
192                    *selection,
193                ));
194            }
195        } else if let Some(msg) = message.data::<PopupMessage>() {
196            if message.destination() == self.popup {
197                match msg {
198                    PopupMessage::Open => {
199                        ui.send_message(DropdownListMessage::open(
200                            self.handle,
201                            MessageDirection::FromWidget,
202                        ));
203                    }
204                    PopupMessage::Close => {
205                        ui.send_message(DropdownListMessage::open(
206                            self.handle,
207                            MessageDirection::FromWidget,
208                        ));
209                    }
210                    _ => (),
211                }
212            }
213        }
214    }
215}
216
217impl DropdownList {
218    pub fn selection(&self) -> Option<usize> {
219        self.selection
220    }
221
222    pub fn close_on_selection(&self) -> bool {
223        self.close_on_selection
224    }
225
226    pub fn items(&self) -> &[Handle<UiNode>] {
227        &self.items
228    }
229}
230
231pub struct DropdownListBuilder {
232    widget_builder: WidgetBuilder,
233    items: Vec<Handle<UiNode>>,
234    selected: Option<usize>,
235    close_on_selection: bool,
236}
237
238impl DropdownListBuilder {
239    pub fn new(widget_builder: WidgetBuilder) -> Self {
240        Self {
241            widget_builder,
242            items: Default::default(),
243            selected: None,
244            close_on_selection: false,
245        }
246    }
247
248    pub fn with_items(mut self, items: Vec<Handle<UiNode>>) -> Self {
249        self.items = items;
250        self
251    }
252
253    pub fn with_selected(mut self, index: usize) -> Self {
254        self.selected = Some(index);
255        self
256    }
257
258    pub fn with_opt_selected(mut self, index: Option<usize>) -> Self {
259        self.selected = index;
260        self
261    }
262
263    pub fn with_close_on_selection(mut self, value: bool) -> Self {
264        self.close_on_selection = value;
265        self
266    }
267
268    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode>
269    where
270        Self: Sized,
271    {
272        let items_control = ListViewBuilder::new(
273            WidgetBuilder::new().with_max_size(Vector2::new(f32::INFINITY, 200.0)),
274        )
275        .with_items(self.items.clone())
276        .build(ctx);
277
278        let popup = PopupBuilder::new(WidgetBuilder::new())
279            .with_content(items_control)
280            .build(ctx);
281
282        let current = if let Some(selected) = self.selected {
283            self.items
284                .get(selected)
285                .map_or(Handle::NONE, |&f| ctx.copy(f))
286        } else {
287            Handle::NONE
288        };
289
290        let arrow = make_arrow(ctx, ArrowDirection::Bottom, 10.0);
291        ctx[arrow].set_column(1);
292
293        let main_grid =
294            GridBuilder::new(WidgetBuilder::new().with_child(current).with_child(arrow))
295                .add_row(Row::stretch())
296                .add_column(Column::stretch())
297                .add_column(Column::strict(20.0))
298                .build(ctx);
299
300        let dropdown_list = UiNode::new(DropdownList {
301            widget: self
302                .widget_builder
303                .with_preview_messages(true)
304                .with_child(
305                    BorderBuilder::new(
306                        WidgetBuilder::new()
307                            .with_foreground(BRUSH_LIGHT)
308                            .with_child(main_grid),
309                    )
310                    .build(ctx),
311                )
312                .build(),
313            popup,
314            items: self.items,
315            list_view: items_control,
316            current,
317            selection: self.selected,
318            close_on_selection: self.close_on_selection,
319            main_grid,
320        });
321
322        ctx.add_node(dropdown_list)
323    }
324}