vizia_core/views/
dropdown.rs

1use crate::prelude::*;
2
3/// A dropdown is used to display some state with the ability to open a popup with options to change that state.
4///
5/// Usually a dropdown is used in the context of a "combobox" or "picklist" to allow the user to select
6/// from one of several discrete options. The dropdown takes two closures, one which shows the current state
7/// regardless of whether the dropdown is open or closed, and one which shows the contents while it is open.
8///
9/// ## Basic Dropdown
10///
11/// A basic dropdown displaying five options that the user can choose from.
12///
13/// ```
14/// # use vizia_core::prelude::*;
15/// #
16/// # #[derive(Lens)]
17/// # struct AppData {
18/// #     value: u8,
19/// # }
20/// #
21/// # impl Model for AppData {}
22/// #
23/// # enum AppEvent {
24/// #     SetValue(u8),
25/// # }
26/// #
27/// # let cx = &mut Context::default();
28/// #
29/// # AppData { value: 0 }.build(cx);
30/// #
31/// Dropdown::new(
32///     cx,
33///     |cx| Label::new(cx, AppData::value),
34///     |cx| {
35///         for i in 0..5 {
36///             Label::new(cx, i)
37///                 .on_press(move |cx| {
38///                     cx.emit(AppEvent::SetValue(i));
39///                     cx.emit(PopupEvent::Close); // close the popup
40///                 })
41///                 .width(Stretch(1.0));
42///         }
43///     },
44/// )
45/// .width(Pixels(100.0));
46/// ```
47///
48/// The line marked "close the popup" is not required for anything other than closing the popup -
49/// if you leave it out, the popup will simply not close until the user clicks out of the dropdown.
50///
51/// ## Custom Dropdown
52///
53/// The dropdown doesn't have to be the current state and then a set of options - it can contain any
54/// set of views in either location. Here's an example where you can use a textbox to filter a list
55/// of checkboxes which pop up when you click the textbox:
56///
57/// ```
58/// # use vizia_core::prelude::*;
59/// # let cx = &mut Context::default();
60///
61/// #[derive(Lens, Clone, PartialEq, Eq)]
62/// struct AppData {
63///     values: [bool; 6],
64///     filter: String,
65/// }
66///
67/// # impl Data for AppData {
68/// #     fn same(&self, other: &Self) -> bool {
69/// #         self == other
70/// #     }
71/// # }
72/// #
73/// # #[derive(Debug)]
74/// # enum AppEvent {
75/// #     SetFilter(String),
76/// #     SetValue(usize, bool),
77/// # }
78/// #
79/// # impl Model for AppData {
80/// #     fn event(&mut self, _cx: &mut EventContext, event: &mut Event) {
81/// #         event.map(|msg, _| {
82/// #             match msg {
83/// #                 AppEvent::SetFilter(s) => self.filter = s.clone(),
84/// #                 AppEvent::SetValue(i, b) => self.values[*i] = *b,
85/// #             }
86/// #         });
87/// #     }
88/// # }
89/// #
90/// # const LABELS: [&str; 6] = ["Bees", "Butterflies", "Dragonflies", "Crickets", "Moths", "Ladybugs"];
91/// #
92/// # AppData {
93/// #     values: [true, false, true, false, true, false],
94/// #     filter: "".to_owned(),
95/// # }.build(cx);
96///
97/// Dropdown::new(cx, |cx| {
98///     Textbox::new(cx, AppData::filter).on_edit(|cx, text| {
99///         cx.emit(AppEvent::SetFilter(text));
100///     })
101///     .width(Pixels(100.0))
102///     .height(Pixels(30.0))
103/// }, |cx| {
104///     Binding::new(cx, AppData::root, |cx, lens| {
105///         let current = lens.get(cx);
106///         for i in 0..6 {
107///             if LABELS[i].to_lowercase().contains(&current.filter.to_lowercase()) {
108///                 HStack::new(cx, move |cx| {
109///                     Checkbox::new(cx, AppData::values.map(move |x| x[i]))
110///                         .on_toggle(move |cx| {
111///                             cx.emit(AppEvent::SetValue(i, !current.values[i]));
112///                         });
113///                     Label::new(cx, LABELS[i]);
114///                 });
115///             }
116///         }
117///     });
118/// }).width(Pixels(100.0));
119/// ```
120#[derive(Lens)]
121pub struct Dropdown {
122    pub is_open: PopupData,
123    pub placement: Placement,
124    pub show_arrow: bool,
125    pub arrow_size: Length,
126    pub should_reposition: bool,
127}
128
129impl Dropdown {
130    /// Creates a new dropdown.
131    ///
132    /// # Example
133    ///
134    /// ```
135    /// # use vizia_core::prelude::*;
136    /// #
137    /// # let cx = &mut Context::default();
138    /// #
139    /// Dropdown::new(cx, |cx| Label::new(cx, "Text"), |_| {});
140    /// ```
141    pub fn new<F, L>(cx: &mut Context, trigger: L, content: F) -> Handle<Self>
142    where
143        L: 'static + Fn(&mut Context),
144        F: 'static + Fn(&mut Context),
145    {
146        Self {
147            is_open: PopupData::default(),
148            placement: Placement::Bottom,
149            show_arrow: true,
150            arrow_size: Length::Value(LengthValue::Px(4.0)),
151            should_reposition: true,
152        }
153        .build(cx, move |cx| {
154            (trigger)(cx);
155
156            Binding::new(cx, Self::is_open, move |cx, is_open| {
157                if is_open.get(cx).into() {
158                    Popup::new(cx, |cx| {
159                        (content)(cx);
160                    })
161                    .on_blur(|cx| cx.emit(PopupEvent::Close))
162                    .placement(Dropdown::placement)
163                    .show_arrow(Dropdown::show_arrow)
164                    .arrow_size(Dropdown::arrow_size)
165                    .should_reposition(Dropdown::should_reposition);
166                }
167            })
168        })
169    }
170}
171
172impl View for Dropdown {
173    fn element(&self) -> Option<&'static str> {
174        Some("dropdown")
175    }
176
177    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
178        self.is_open.event(cx, event);
179    }
180}
181
182impl Handle<'_, Dropdown> {
183    /// Sets the position where the tooltip should appear relative to its parent element.
184    /// Defaults to `Placement::Bottom`.
185    pub fn placement(self, placement: impl Res<Placement>) -> Self {
186        self.bind(placement, |handle, placement| {
187            let placement = placement.get(&handle);
188            handle.modify(|dropdown| {
189                dropdown.placement = placement;
190            });
191        })
192    }
193
194    /// Sets whether the popup should include an arrow. Defaults to true.
195    pub fn show_arrow(self, show_arrow: bool) -> Self {
196        self.modify(|dropdown| dropdown.show_arrow = show_arrow)
197    }
198
199    /// Sets the size of the popup arrow, or gap if the arrow is hidden.
200    pub fn arrow_size(self, size: impl Into<Length>) -> Self {
201        self.modify(|dropdown| dropdown.arrow_size = size.into())
202    }
203
204    /// Set to whether the popup should reposition to always be visible.
205    pub fn should_reposition(self, flag: bool) -> Self {
206        self.modify(|dropdown| dropdown.should_reposition = flag)
207    }
208}