Skip to main content

vizia_core/views/
picklist.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 PickList {
9    on_select: Option<Box<dyn Fn(&mut EventContext, usize)>>,
10    placeholder: Signal<String>,
11    is_open: Signal<bool>,
12}
13
14pub(crate) enum PickListEvent {
15    SetOption(usize),
16}
17
18impl PickList {
19    /// Creates a new [PickList] view.
20    pub fn new<L, S, V, T>(
21        cx: &mut Context,
22        list: L,
23        selected: S,
24        show_handle: bool,
25    ) -> Handle<Self>
26    where
27        L: SignalGet<V> + SignalMap<V> + Res<V> + 'static,
28        V: Deref<Target = [T]> + Clone + 'static,
29        T: 'static + ToStringLocalized + Clone + PartialEq,
30        S: Res<usize> + 'static,
31    {
32        let is_open = Signal::new(false);
33        let placeholder = Signal::new(String::new());
34        let selected = selected.to_signal(cx);
35        Self { on_select: None, placeholder, is_open }
36            .build(cx, |cx| {
37                Button::new(cx, |cx| {
38                    // A Label and an optional Icon
39                    HStack::new(cx, move |cx| {
40                        Label::new(cx, placeholder)
41                            .bind(list, move |handle| {
42                                let list = list.get();
43                                handle.bind(selected, move |handle| {
44                                    let selected_index = selected.get();
45                                    let list_len = list.len();
46                                    if selected_index < list_len {
47                                        let selected_text =
48                                            if let Some(item) = list.get(selected_index) {
49                                                item.to_string_local(&handle)
50                                            } else {
51                                                placeholder.get()
52                                            };
53                                        handle.text(selected_text);
54                                    } else {
55                                        handle.text(placeholder.get());
56                                    }
57                                });
58                            })
59                            .width(Stretch(2.0))
60                            .text_wrap(false)
61                            .text_overflow(TextOverflow::Ellipsis)
62                            .hoverable(false);
63                        if show_handle {
64                            Svg::new(cx, ICON_CHEVRON_DOWN)
65                                .class("icon")
66                                .size(Pixels(16.0))
67                                .hoverable(false);
68                        }
69                    })
70                    .width(Stretch(1.0))
71                    //.gap(Stretch(1.0))
72                    .gap(Pixels(8.0))
73                })
74                .width(Stretch(1.0))
75                .on_press(|cx| cx.emit(PopupEvent::Open));
76
77                Binding::new(cx, is_open, move |cx| {
78                    let is_open = is_open.get();
79                    if is_open {
80                        Popup::new(cx, |cx| {
81                            List::new(cx, list, move |cx, _, item| {
82                                Element::new(cx).class("focus-indicator");
83                                Svg::new(cx, ICON_CHECK).class("checkmark").size(Pixels(16.0));
84                                Label::new(cx, item.map(|v| v.clone())).hoverable(false);
85                            })
86                            .selectable(Selectable::Single)
87                            .selected(selected.map(|s| vec![*s]))
88                            .on_select(|cx, index| {
89                                cx.emit(PickListEvent::SetOption(index));
90                                cx.emit(PopupEvent::Close);
91                            })
92                            .focused(true);
93                        })
94                        .arrow_size(Pixels(4.0))
95                        .on_blur(|cx| cx.emit(PopupEvent::Close));
96                    }
97                });
98            })
99            .navigable(false)
100    }
101}
102
103impl View for PickList {
104    fn element(&self) -> Option<&'static str> {
105        Some("picklist")
106    }
107
108    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
109        event.map(|picklist_event, _| match picklist_event {
110            PickListEvent::SetOption(index) => {
111                if let Some(callback) = &self.on_select {
112                    (callback)(cx, *index);
113                }
114            }
115        });
116
117        event.map(|popup_event, meta| match popup_event {
118            PopupEvent::Open => {
119                self.is_open.set_if_changed(true);
120
121                meta.consume();
122            }
123
124            PopupEvent::Close => {
125                self.is_open.set_if_changed(false);
126                let e = cx.first_child();
127                cx.with_current(e, |cx| cx.focus());
128                meta.consume();
129            }
130
131            PopupEvent::Switch => {
132                self.is_open.set(!self.is_open.get());
133                meta.consume();
134            }
135        });
136    }
137}
138
139impl Handle<'_, PickList> {
140    /// Sets the placeholder text that appears when the textbox has no value.
141    pub fn placeholder<P: ToStringLocalized + Clone + 'static>(
142        self,
143        placeholder: impl Res<P> + 'static,
144    ) -> Self {
145        let placeholder = placeholder.to_signal(self.cx);
146        self.bind(placeholder, move |handle| {
147            let val = placeholder.get();
148            let txt = val.to_string_local(&handle);
149            handle.modify(|picklist| picklist.placeholder.set(txt));
150        })
151    }
152
153    /// Sets the callback triggered when an option is selected.
154    pub fn on_select<F>(self, callback: F) -> Self
155    where
156        F: 'static + Fn(&mut EventContext, usize),
157    {
158        self.modify(|picklist: &mut PickList| picklist.on_select = Some(Box::new(callback)))
159    }
160}