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}