Skip to main content

vizia_core/views/
select.rs

1use std::ops::Deref;
2
3use crate::context::TreeProps;
4use crate::icons::{ICON_CHECK, ICON_CHEVRON_DOWN};
5use crate::prelude::*;
6
7/// A view which allows the user to select an item from a dropdown list.
8pub struct Select {
9    on_select: Option<Box<dyn Fn(&mut EventContext, usize)>>,
10    placeholder: Signal<String>,
11    is_open: Signal<bool>,
12    min_selected: Signal<usize>,
13    max_selected: Signal<usize>,
14}
15
16pub(crate) enum SelectEvent {
17    SetOption(usize),
18}
19
20impl Select {
21    /// Creates a new [Select] view.
22    pub fn new<L, S, V, T>(
23        cx: &mut Context,
24        list: L,
25        selected: S,
26        show_handle: bool,
27    ) -> Handle<Self>
28    where
29        L: SignalGet<V> + SignalMap<V> + Res<V> + 'static,
30        V: Deref<Target = [T]> + Clone + 'static,
31        T: 'static + ToStringLocalized + Clone + PartialEq,
32        S: Res<Option<usize>> + 'static,
33    {
34        let list = list.to_signal(cx);
35        let is_open = Signal::new(false);
36        let placeholder = Signal::new(String::new());
37        let display_text = Signal::new(String::new());
38        let min_selected = Signal::new(0);
39        let max_selected = Signal::new(usize::MAX);
40        let selected = selected.to_signal(cx);
41        let locale = cx.environment().locale;
42        Self { on_select: None, placeholder, is_open, min_selected, max_selected }
43            .build(cx, |cx| {
44                Button::new(cx, |cx| {
45                    // A Label and an optional Icon
46                    HStack::new(cx, move |cx| {
47                        Label::new(cx, display_text)
48                            .bind(list, move |handle| {
49                                let text = if let Some(index) = selected.get() {
50                                    list.with(|list| list.get(index).cloned())
51                                        .map(|item| item.to_string_local(&handle))
52                                        .unwrap_or_else(|| placeholder.get())
53                                } else {
54                                    placeholder.get()
55                                };
56
57                                display_text.set_if_changed(text);
58                            })
59                            .bind(selected, move |handle| {
60                                let text = if let Some(index) = selected.get() {
61                                    list.with(|list| list.get(index).cloned())
62                                        .map(|item| item.to_string_local(&handle))
63                                        .unwrap_or_else(|| placeholder.get())
64                                } else {
65                                    placeholder.get()
66                                };
67
68                                display_text.set_if_changed(text);
69                            })
70                            .bind(placeholder, move |handle| {
71                                let text = if let Some(index) = selected.get() {
72                                    list.with(|list| list.get(index).cloned())
73                                        .map(|item| item.to_string_local(&handle))
74                                        .unwrap_or_else(|| placeholder.get())
75                                } else {
76                                    placeholder.get()
77                                };
78
79                                display_text.set_if_changed(text);
80                            })
81                            .bind(locale, move |handle| {
82                                let text = if let Some(index) = selected.get() {
83                                    list.with(|list| list.get(index).cloned())
84                                        .map(|item| item.to_string_local(&handle))
85                                        .unwrap_or_else(|| placeholder.get())
86                                } else {
87                                    placeholder.get()
88                                };
89
90                                display_text.set_if_changed(text);
91                            })
92                            .width(Stretch(2.0))
93                            .text_wrap(false)
94                            .text_overflow(TextOverflow::Ellipsis)
95                            .hoverable(false);
96                        if show_handle {
97                            Svg::new(cx, ICON_CHEVRON_DOWN)
98                                .class("icon")
99                                .size(Pixels(16.0))
100                                .hoverable(false);
101                        }
102                    })
103                    .width(Stretch(1.0))
104                    //.gap(Stretch(1.0))
105                    .gap(Pixels(8.0))
106                })
107                .variant(ButtonVariant::Outline)
108                .width(Stretch(1.0))
109                .on_press(|cx| cx.emit(PopupEvent::Open));
110
111                Binding::new(cx, is_open, move |cx| {
112                    let is_open = is_open.get();
113                    if is_open {
114                        Popover::new(cx, |cx| {
115                            let list_signal = list;
116
117                            List::new(cx, list_signal, move |cx, _, item| {
118                                Svg::new(cx, ICON_CHECK).class("checkmark").size(Pixels(16.0));
119                                Label::new(cx, item.map(|v| v.clone())).hoverable(false);
120                            })
121                            .selectable(Selectable::Single)
122                            .min_selected(min_selected)
123                            .max_selected(max_selected)
124                            .type_ahead_text(move |cx, index| {
125                                list_signal.with(|list| {
126                                    list.get(index).map(|item| item.to_string_local(cx))
127                                })
128                            })
129                            .focus_first_item_on_focus_in(false)
130                            .selection(
131                                selected.map(|s| {
132                                    if let Some(index) = s { vec![*index] } else { vec![] }
133                                }),
134                            )
135                            .on_select(|cx, index| {
136                                cx.emit(SelectEvent::SetOption(index));
137                                cx.emit(PopupEvent::Close);
138                            })
139                            .focused(true);
140                        })
141                        .arrow_size(Pixels(4.0))
142                        .on_blur(|cx| cx.emit(PopupEvent::Close));
143                    }
144                });
145            })
146            .navigable(false)
147    }
148}
149
150impl View for Select {
151    fn element(&self) -> Option<&'static str> {
152        Some("select")
153    }
154
155    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
156        event.map(|select_event, _| match select_event {
157            SelectEvent::SetOption(index) => {
158                if let Some(callback) = &self.on_select {
159                    (callback)(cx, *index);
160                }
161            }
162        });
163
164        event.map(|popup_event, meta| match popup_event {
165            PopupEvent::Open => {
166                self.is_open.set_if_changed(true);
167
168                meta.consume();
169            }
170
171            PopupEvent::Close => {
172                self.is_open.set_if_changed(false);
173                let e = cx.first_child();
174                cx.with_current(e, |cx| cx.focus());
175                meta.consume();
176            }
177
178            PopupEvent::Switch => {
179                self.is_open.set(!self.is_open.get());
180                meta.consume();
181            }
182        });
183    }
184}
185
186impl Handle<'_, Select> {
187    /// Sets the placeholder text that appears when the textbox has no value.
188    pub fn placeholder<P: ToStringLocalized + Clone + 'static>(
189        self,
190        placeholder: impl Res<P> + 'static,
191    ) -> Self {
192        //let pt = placeholder.get_value(self.cx).to_string_local(self.cx);
193        //println!("Setting initial placeholder: {}", pt);
194        let placeholder = placeholder.to_signal(self.cx);
195        let locale = self.cx.environment().locale;
196
197        self.bind(placeholder, move |handle| {
198            let val = placeholder.get();
199            let txt = val.to_string_local(&handle);
200            handle.modify(|select| select.placeholder.set(txt));
201        })
202        .bind(locale, move |handle| {
203            let val = placeholder.get();
204            let txt = val.to_string_local(&handle);
205            handle.modify(|select| select.placeholder.set(txt));
206        })
207    }
208
209    /// Sets the callback triggered when an option is selected.
210    pub fn on_select<F>(self, callback: F) -> Self
211    where
212        F: 'static + Fn(&mut EventContext, usize),
213    {
214        self.modify(|select: &mut Select| select.on_select = Some(Box::new(callback)))
215    }
216
217    /// Sets the minimum number of selected items.
218    pub fn min_selected(self, min_selected: impl Res<usize> + 'static) -> Self {
219        let min_selected = min_selected.to_signal(self.cx);
220        self.bind(min_selected, move |handle| {
221            let min_selected = min_selected.get();
222            handle.modify(|select: &mut Select| select.min_selected.set(min_selected));
223        })
224    }
225
226    /// Sets the maximum number of selected items.
227    pub fn max_selected(self, max_selected: impl Res<usize> + 'static) -> Self {
228        let max_selected = max_selected.to_signal(self.cx);
229        self.bind(max_selected, move |handle| {
230            let max_selected = max_selected.get();
231            handle.modify(|select: &mut Select| select.max_selected.set(max_selected));
232        })
233    }
234}