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