pixel_widgets/widget/
dropdown.rs

1use smallvec::smallvec;
2
3use crate::draw::Primitive;
4use crate::event::{Event, Key};
5use crate::layout::{Rectangle, Size};
6use crate::node::{GenericNode, IntoNode, Node};
7use crate::style::{StyleState, Stylesheet};
8use crate::widget::{Context, StateVec, Widget};
9
10/// Pick an item from a dropdown box
11pub struct Dropdown<'a, T, F> {
12    items: Vec<Node<'a, T>>,
13    default_selection: Option<usize>,
14    on_select: F,
15}
16
17/// State for [`Dropdown`](struct.Dropdown.html).
18pub struct State {
19    selected_item: Option<usize>,
20    hovered: bool,
21    inner: InnerState,
22}
23
24enum InnerState {
25    Idle,
26    Open { scroll: f32, hover_item: usize },
27    Pressed { scroll: f32, hover_item: usize },
28}
29
30impl<'a, T: 'a, F> Dropdown<'a, T, F> {
31    /// Set the default selected item
32    pub fn default_selection(mut self, item_index: usize) -> Self {
33        self.default_selection = Some(item_index);
34        self
35    }
36
37    /// Sets the on_select callback for the dropdown, which is called when an item is selected
38    pub fn on_select<N: Fn(usize) -> T>(self, on_select: N) -> Dropdown<'a, T, N> {
39        Dropdown {
40            items: self.items,
41            default_selection: self.default_selection,
42            on_select,
43        }
44    }
45
46    /// Add an item to the dropdown.
47    pub fn push(mut self, item: impl IntoNode<'a, T>) -> Self {
48        self.items.push(item.into_node());
49        self
50    }
51
52    /// Add multiple items to the dropdown.
53    pub fn extend(mut self, items: impl IntoIterator<Item = impl IntoNode<'a, T>>) -> Self {
54        self.items.extend(items.into_iter().map(IntoNode::into_node));
55        self
56    }
57}
58
59impl<'a, T: 'a> Default for Dropdown<'a, T, fn(usize) -> T> {
60    fn default() -> Self {
61        Self {
62            items: Vec::new(),
63            default_selection: None,
64            on_select: |_| panic!("on_select of `Dropdown` must be set"),
65        }
66    }
67}
68
69impl<'a, T: Send + 'a, F: Send + Fn(usize) -> T> Widget<'a, T> for Dropdown<'a, T, F> {
70    type State = State;
71
72    fn mount(&self) -> Self::State {
73        State {
74            selected_item: self.default_selection.map(|i| i.min(self.items.len() - 1)),
75            ..Default::default()
76        }
77    }
78
79    fn widget(&self) -> &'static str {
80        "dropdown"
81    }
82
83    fn state(&self, state: &State) -> StateVec {
84        match state.inner {
85            InnerState::Open { .. } | InnerState::Pressed { .. } => smallvec![StyleState::Open],
86            InnerState::Idle if state.hovered => smallvec![StyleState::Hover],
87            InnerState::Idle => StateVec::new(),
88        }
89    }
90
91    fn len(&self) -> usize {
92        self.items.len()
93    }
94
95    fn visit_children(&mut self, visitor: &mut dyn FnMut(&mut dyn GenericNode<'a, T>)) {
96        for item in self.items.iter_mut() {
97            visitor(&mut **item);
98        }
99    }
100
101    fn size(&self, _: &State, style: &Stylesheet) -> (Size, Size) {
102        let width = match style.width {
103            Size::Shrink => Size::Exact(self.items.iter().fold(0.0f32, |size, item| match item.size().0 {
104                Size::Exact(item_size) => size.max(item_size),
105                _ => size,
106            })),
107            other => other,
108        };
109        let height = match style.height {
110            Size::Shrink => Size::Exact(self.items.iter().fold(0.0f32, |size, item| match item.size().1 {
111                Size::Exact(item_size) => size.max(item_size),
112                _ => size,
113            })),
114            other => other,
115        };
116
117        style
118            .background
119            .resolve_size((style.width, style.height), (width, height), style.padding)
120    }
121
122    fn hit(&self, state: &State, layout: Rectangle, clip: Rectangle, _: &Stylesheet, x: f32, y: f32) -> bool {
123        self.focused(state) || (layout.point_inside(x, y) && clip.point_inside(x, y))
124    }
125
126    fn focused(&self, state: &State) -> bool {
127        matches!(state.inner, InnerState::Open { .. } | InnerState::Pressed { .. })
128    }
129
130    fn event(
131        &mut self,
132        state: &mut State,
133        layout: Rectangle,
134        clip: Rectangle,
135        _: &Stylesheet,
136        event: Event,
137        context: &mut Context<T>,
138    ) {
139        state.inner = match (event, std::mem::replace(&mut state.inner, InnerState::Idle)) {
140            (Event::Cursor(x, y), InnerState::Idle) => {
141                let hovered = layout.point_inside(x, y) && clip.point_inside(x, y);
142                if hovered != state.hovered {
143                    context.redraw();
144                    state.hovered = hovered;
145                }
146                InnerState::Idle
147            }
148
149            (Event::Cursor(x, y), InnerState::Open { scroll, hover_item }) => {
150                let hovered = x >= layout.left
151                    && x < layout.right
152                    && y >= layout.bottom
153                    && y < layout.bottom + self.items.len() as f32 * layout.height();
154                if hovered != state.hovered {
155                    context.redraw();
156                    state.hovered = hovered;
157                }
158
159                let new_hover_item =
160                    (((y - layout.bottom) / layout.height()).floor().max(0.0) as usize).min(self.items.len() - 1);
161
162                if new_hover_item != hover_item {
163                    context.redraw();
164                    InnerState::Open {
165                        scroll,
166                        hover_item: new_hover_item,
167                    }
168                } else {
169                    InnerState::Open { scroll, hover_item }
170                }
171            }
172
173            (Event::Cursor(x, y), InnerState::Pressed { scroll, hover_item }) => {
174                let hovered = x >= layout.left
175                    && x < layout.right
176                    && y >= layout.bottom
177                    && y < layout.bottom + self.items.len() as f32 * layout.height();
178                if hovered != state.hovered {
179                    context.redraw();
180                    state.hovered = hovered;
181                }
182
183                let new_hover_item =
184                    (((y - layout.bottom) / layout.height()).floor().max(0.0) as usize).min(self.items.len() - 1);
185
186                if new_hover_item != hover_item || !state.hovered {
187                    context.redraw();
188                    InnerState::Open {
189                        scroll,
190                        hover_item: new_hover_item,
191                    }
192                } else {
193                    InnerState::Pressed { scroll, hover_item }
194                }
195            }
196
197            (Event::Press(Key::LeftMouseButton), InnerState::Idle) => {
198                if state.hovered {
199                    context.redraw();
200                    InnerState::Open {
201                        scroll: 0.0,
202                        hover_item: 0,
203                    }
204                } else {
205                    InnerState::Idle
206                }
207            }
208
209            (Event::Press(Key::LeftMouseButton), InnerState::Open { scroll, hover_item }) => {
210                context.redraw();
211                if state.hovered {
212                    InnerState::Pressed { scroll, hover_item }
213                } else {
214                    InnerState::Idle
215                }
216            }
217
218            (Event::Release(Key::LeftMouseButton), InnerState::Pressed { hover_item, .. }) => {
219                context.redraw();
220                state.selected_item.replace(hover_item);
221                context.push((self.on_select)(hover_item));
222                InnerState::Idle
223            }
224
225            (_, state) => state,
226        };
227    }
228
229    fn draw(
230        &mut self,
231        state: &mut State,
232        layout: Rectangle,
233        clip: Rectangle,
234        style: &Stylesheet,
235    ) -> Vec<Primitive<'a>> {
236        let content = style.background.content_rect(layout, style.padding);
237        let focused = self.focused(state);
238
239        let mut result = Vec::new();
240        if focused {
241            result.push(Primitive::LayerUp);
242        }
243        match state.inner {
244            InnerState::Idle => {
245                result.extend(style.background.render(layout));
246                if let Some(selected) = state.selected_item {
247                    result.extend(self.items[selected].draw(content, clip));
248                }
249            }
250            InnerState::Open { hover_item, .. } | InnerState::Pressed { hover_item, .. } => {
251                let padding = style.background.padding();
252                let expanded = Rectangle {
253                    left: layout.left,
254                    top: layout.top,
255                    right: layout.right,
256                    bottom: layout.bottom + self.items.len() as f32 * layout.height() + padding.top + padding.bottom,
257                };
258                result.extend(style.background.render(expanded));
259                for (index, item) in self.items.iter_mut().enumerate() {
260                    if index == hover_item {
261                        result.push(Primitive::DrawRect(
262                            Rectangle {
263                                left: layout.left + padding.left,
264                                top: layout.top + (1 + index) as f32 * layout.height() + padding.top,
265                                right: layout.right - padding.right,
266                                bottom: layout.bottom + (1 + index) as f32 * layout.height() + padding.top,
267                            },
268                            style.color,
269                        ));
270                    }
271
272                    let layout = Rectangle {
273                        left: content.left + padding.left,
274                        top: content.top + (1 + index) as f32 * layout.height() + padding.top,
275                        right: content.right - padding.right,
276                        bottom: content.bottom + (1 + index) as f32 * layout.height(),
277                    };
278                    result.extend(item.draw(layout, clip));
279                }
280            }
281        }
282        if focused {
283            result.push(Primitive::LayerDown);
284        }
285        result
286    }
287}
288
289impl<'a, T: 'a + Send, F: 'a + Send + Fn(usize) -> T> IntoNode<'a, T> for Dropdown<'a, T, F> {
290    fn into_node(self) -> Node<'a, T> {
291        Node::from_widget(self)
292    }
293}
294
295impl Default for State {
296    fn default() -> Self {
297        State {
298            selected_item: None,
299            hovered: false,
300            inner: InnerState::Idle,
301        }
302    }
303}