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 is_open = Signal::new(false);
35        let placeholder = Signal::new(String::new());
36        let min_selected = Signal::new(0);
37        let max_selected = Signal::new(usize::MAX);
38        let selected = selected.to_signal(cx);
39        Self { on_select: None, placeholder, is_open, min_selected, max_selected }
40            .build(cx, |cx| {
41                Button::new(cx, |cx| {
42                    // A Label and an optional Icon
43                    HStack::new(cx, move |cx| {
44                        Label::new(cx, placeholder)
45                            .bind(list, move |handle| {
46                                //let list = list.get();
47                                handle.bind(selected, move |handle| {
48                                    let selected_index = selected.get();
49                                    let list_len = list.with(|list| list.len());
50                                    if let Some(index) = selected_index {
51                                        if index < list_len {
52                                            let item = Memo::new(move |_| {
53                                                list.with(move |list| list.get(index).cloned())
54                                            });
55
56                                            if item.get().is_some() {
57                                                handle
58                                                    .text(item.map(move |it| it.clone().unwrap()));
59                                            } else {
60                                                handle.text(placeholder.get());
61                                            };
62                                        } else {
63                                            handle.text(placeholder.get());
64                                        }
65                                    } else {
66                                        handle.text(placeholder.get());
67                                    }
68                                });
69                            })
70                            .width(Stretch(2.0))
71                            .text_wrap(false)
72                            .text_overflow(TextOverflow::Ellipsis)
73                            .hoverable(false);
74                        if show_handle {
75                            Svg::new(cx, ICON_CHEVRON_DOWN)
76                                .class("icon")
77                                .size(Pixels(16.0))
78                                .hoverable(false);
79                        }
80                    })
81                    .width(Stretch(1.0))
82                    //.gap(Stretch(1.0))
83                    .gap(Pixels(8.0))
84                })
85                .variant(ButtonVariant::Outline)
86                .width(Stretch(1.0))
87                .on_press(|cx| cx.emit(PopupEvent::Open));
88
89                Binding::new(cx, is_open, move |cx| {
90                    let is_open = is_open.get();
91                    if is_open {
92                        Popover::new(cx, |cx| {
93                            List::new(cx, list, move |cx, _, item| {
94                                Svg::new(cx, ICON_CHECK).class("checkmark").size(Pixels(16.0));
95                                Label::new(cx, item.map(|v| v.clone())).hoverable(false);
96                            })
97                            .selectable(Selectable::Single)
98                            .min_selected(min_selected)
99                            .max_selected(max_selected)
100                            .selection(
101                                selected.map(|s| {
102                                    if let Some(index) = s { vec![*index] } else { vec![] }
103                                }),
104                            )
105                            .on_select(|cx, index| {
106                                cx.emit(SelectEvent::SetOption(index));
107                                cx.emit(PopupEvent::Close);
108                            })
109                            .focused(true);
110                        })
111                        .arrow_size(Pixels(4.0))
112                        .on_blur(|cx| cx.emit(PopupEvent::Close));
113                    }
114                });
115            })
116            .navigable(false)
117    }
118}
119
120impl View for Select {
121    fn element(&self) -> Option<&'static str> {
122        Some("select")
123    }
124
125    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
126        event.map(|select_event, _| match select_event {
127            SelectEvent::SetOption(index) => {
128                if let Some(callback) = &self.on_select {
129                    (callback)(cx, *index);
130                }
131            }
132        });
133
134        event.map(|popup_event, meta| match popup_event {
135            PopupEvent::Open => {
136                self.is_open.set_if_changed(true);
137
138                meta.consume();
139            }
140
141            PopupEvent::Close => {
142                self.is_open.set_if_changed(false);
143                let e = cx.first_child();
144                cx.with_current(e, |cx| cx.focus());
145                meta.consume();
146            }
147
148            PopupEvent::Switch => {
149                self.is_open.set(!self.is_open.get());
150                meta.consume();
151            }
152        });
153    }
154}
155
156impl Handle<'_, Select> {
157    /// Sets the placeholder text that appears when the textbox has no value.
158    pub fn placeholder<P: ToStringLocalized + Clone + 'static>(
159        self,
160        placeholder: impl Res<P> + 'static,
161    ) -> Self {
162        let placeholder = placeholder.to_signal(self.cx);
163        self.bind(placeholder, move |handle| {
164            let val = placeholder.get();
165            let txt = val.to_string_local(&handle);
166            handle.modify(|select| select.placeholder.set(txt));
167        })
168    }
169
170    /// Sets the callback triggered when an option is selected.
171    pub fn on_select<F>(self, callback: F) -> Self
172    where
173        F: 'static + Fn(&mut EventContext, usize),
174    {
175        self.modify(|select: &mut Select| select.on_select = Some(Box::new(callback)))
176    }
177
178    /// Sets the minimum number of selected items.
179    pub fn min_selected(self, min_selected: impl Res<usize> + 'static) -> Self {
180        let min_selected = min_selected.to_signal(self.cx);
181        self.bind(min_selected, move |handle| {
182            let min_selected = min_selected.get();
183            handle.modify(|select: &mut Select| select.min_selected.set(min_selected));
184        })
185    }
186
187    /// Sets the maximum number of selected items.
188    pub fn max_selected(self, max_selected: impl Res<usize> + 'static) -> Self {
189        let max_selected = max_selected.to_signal(self.cx);
190        self.bind(max_selected, move |handle| {
191            let max_selected = max_selected.get();
192            handle.modify(|select: &mut Select| select.max_selected.set(max_selected));
193        })
194    }
195}