Skip to main content

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(&current.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}