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(¤t.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}