iced_aw/widget/
drop_down.rs

1//! Drop down menu widget
2//!
3//! *This API requires the following crate features to be activated: `drop_down`*
4
5use iced_core::{
6    Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget,
7    keyboard::{self, key::Named},
8    layout::{Limits, Node},
9    mouse::{self, Cursor},
10    overlay, renderer, touch,
11    widget::{Operation, Tree},
12};
13
14pub use crate::core::{alignment::Alignment, offset::Offset};
15
16/// Customizable drop down menu widget
17pub struct DropDown<'a, Message, Theme = iced_widget::Theme, Renderer = iced_widget::Renderer>
18where
19    Message: Clone,
20    Renderer: renderer::Renderer,
21{
22    underlay: Element<'a, Message, Theme, Renderer>,
23    overlay: Element<'a, Message, Theme, Renderer>,
24    on_dismiss: Option<Message>,
25    width: Option<Length>,
26    height: Length,
27    alignment: Alignment,
28    offset: Offset,
29    expanded: bool,
30}
31
32impl<'a, Message, Theme, Renderer> DropDown<'a, Message, Theme, Renderer>
33where
34    Message: Clone,
35    Renderer: renderer::Renderer,
36{
37    /// Create a new [`DropDown`]
38    pub fn new<U, B>(underlay: U, overlay: B, expanded: bool) -> Self
39    where
40        U: Into<Element<'a, Message, Theme, Renderer>>,
41        B: Into<Element<'a, Message, Theme, Renderer>>,
42    {
43        DropDown {
44            underlay: underlay.into(),
45            overlay: overlay.into(),
46            expanded,
47            on_dismiss: None,
48            width: None,
49            height: Length::Shrink,
50            alignment: Alignment::Bottom,
51            offset: Offset::from(5.0),
52        }
53    }
54
55    /// The width of the overlay
56    #[must_use]
57    pub fn width(mut self, width: impl Into<Length>) -> Self {
58        self.width = Some(width.into());
59        self
60    }
61
62    /// The height of the overlay
63    #[must_use]
64    pub fn height(mut self, height: impl Into<Length>) -> Self {
65        self.height = height.into();
66        self
67    }
68
69    /// The alignment of the overlay relative to the underlay
70    #[must_use]
71    pub fn alignment(mut self, alignment: impl Into<Alignment>) -> Self {
72        self.alignment = alignment.into();
73        self
74    }
75
76    /// The offset of the overlay
77    #[must_use]
78    pub fn offset(mut self, offset: impl Into<Offset>) -> Self {
79        self.offset = offset.into();
80        self
81    }
82
83    /// Send a message when a click occur outside of the overlay when expanded
84    #[must_use]
85    pub fn on_dismiss(mut self, message: Message) -> Self {
86        self.on_dismiss = Some(message);
87        self
88    }
89}
90
91impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
92    for DropDown<'a, Message, Theme, Renderer>
93where
94    Message: 'a + Clone,
95    Renderer: 'a + renderer::Renderer,
96{
97    fn size(&self) -> Size<Length> {
98        self.underlay.as_widget().size()
99    }
100
101    fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
102        self.underlay
103            .as_widget_mut()
104            .layout(&mut tree.children[0], renderer, limits)
105    }
106
107    fn draw(
108        &self,
109        state: &Tree,
110        renderer: &mut Renderer,
111        theme: &Theme,
112        style: &renderer::Style,
113        layout: Layout<'_>,
114        cursor: Cursor,
115        viewport: &Rectangle,
116    ) {
117        self.underlay.as_widget().draw(
118            &state.children[0],
119            renderer,
120            theme,
121            style,
122            layout,
123            cursor,
124            viewport,
125        );
126    }
127
128    fn children(&self) -> Vec<Tree> {
129        vec![Tree::new(&self.underlay), Tree::new(&self.overlay)]
130    }
131
132    fn diff(&self, tree: &mut Tree) {
133        tree.diff_children(&[&self.underlay, &self.overlay]);
134    }
135
136    fn operate<'b>(
137        &'b mut self,
138        state: &'b mut Tree,
139        layout: Layout<'_>,
140        renderer: &Renderer,
141        operation: &mut dyn Operation<()>,
142    ) {
143        self.underlay
144            .as_widget_mut()
145            .operate(&mut state.children[0], layout, renderer, operation);
146    }
147
148    fn update(
149        &mut self,
150        state: &mut Tree,
151        event: &Event,
152        layout: Layout<'_>,
153        cursor: Cursor,
154        renderer: &Renderer,
155        clipboard: &mut dyn Clipboard,
156        shell: &mut Shell<'_, Message>,
157        viewport: &Rectangle,
158    ) {
159        self.underlay.as_widget_mut().update(
160            &mut state.children[0],
161            event,
162            layout,
163            cursor,
164            renderer,
165            clipboard,
166            shell,
167            viewport,
168        );
169    }
170
171    fn mouse_interaction(
172        &self,
173        state: &Tree,
174        layout: Layout<'_>,
175        cursor: Cursor,
176        viewport: &Rectangle,
177        renderer: &Renderer,
178    ) -> mouse::Interaction {
179        self.underlay.as_widget().mouse_interaction(
180            &state.children[0],
181            layout,
182            cursor,
183            viewport,
184            renderer,
185        )
186    }
187
188    fn overlay<'b>(
189        &'b mut self,
190        state: &'b mut Tree,
191        layout: Layout<'b>,
192        renderer: &Renderer,
193        viewport: &Rectangle,
194        translation: Vector,
195    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
196        if !self.expanded {
197            return self.underlay.as_widget_mut().overlay(
198                &mut state.children[0],
199                layout,
200                renderer,
201                viewport,
202                translation,
203            );
204        }
205
206        Some(overlay::Element::new(Box::new(DropDownOverlay::new(
207            &mut state.children[1],
208            &mut self.overlay,
209            self.on_dismiss.as_ref(),
210            self.width.as_ref(),
211            &self.height,
212            &self.alignment,
213            &self.offset,
214            layout.bounds(),
215            layout.position() + translation,
216            *viewport,
217        ))))
218    }
219}
220
221impl<'a, Message, Theme: 'a, Renderer> From<DropDown<'a, Message, Theme, Renderer>>
222    for Element<'a, Message, Theme, Renderer>
223where
224    Message: 'a + Clone,
225    Renderer: 'a + renderer::Renderer,
226{
227    fn from(drop_down: DropDown<'a, Message, Theme, Renderer>) -> Self {
228        Element::new(drop_down)
229    }
230}
231
232struct DropDownOverlay<
233    'a,
234    'b,
235    Message,
236    Theme = iced_widget::Theme,
237    Renderer = iced_widget::Renderer,
238> where
239    Message: Clone,
240{
241    state: &'b mut Tree,
242    element: &'b mut Element<'a, Message, Theme, Renderer>,
243    on_dismiss: Option<&'b Message>,
244    width: Option<&'b Length>,
245    height: &'b Length,
246    alignment: &'b Alignment,
247    offset: &'b Offset,
248    underlay_bounds: Rectangle,
249    position: Point,
250    viewport: Rectangle,
251}
252
253impl<'a, 'b, Message, Theme, Renderer> DropDownOverlay<'a, 'b, Message, Theme, Renderer>
254where
255    Message: Clone,
256    Renderer: renderer::Renderer,
257{
258    #[allow(clippy::too_many_arguments)]
259    fn new(
260        state: &'b mut Tree,
261        element: &'b mut Element<'a, Message, Theme, Renderer>,
262        on_dismiss: Option<&'b Message>,
263        width: Option<&'b Length>,
264        height: &'b Length,
265        alignment: &'b Alignment,
266        offset: &'b Offset,
267        underlay_bounds: Rectangle,
268        position: Point,
269        viewport: Rectangle,
270    ) -> Self {
271        DropDownOverlay {
272            state,
273            element,
274            on_dismiss,
275            width,
276            height,
277            alignment,
278            offset,
279            underlay_bounds,
280            position,
281            viewport,
282        }
283    }
284}
285
286impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
287    for DropDownOverlay<'_, '_, Message, Theme, Renderer>
288where
289    Message: Clone,
290    Renderer: renderer::Renderer,
291{
292    fn layout(&mut self, renderer: &Renderer, bounds: Size) -> Node {
293        let limits = Limits::new(Size::ZERO, bounds)
294            .width(
295                *self
296                    .width
297                    .unwrap_or(&Length::Fixed(self.underlay_bounds.width)),
298            )
299            .height(*self.height);
300
301        let previous_position = self.position;
302        let max = limits.max();
303
304        let height_above = (previous_position.y - self.offset.y).max(0.0);
305        let height_below =
306            (max.height - previous_position.y - self.underlay_bounds.height - self.offset.y)
307                .max(0.0);
308
309        let ref_center_y = previous_position.y + self.underlay_bounds.height / 2.0;
310        let max_height_symmetric = (ref_center_y.min(max.height - ref_center_y) * 2.0).max(0.0);
311
312        let limits = match self.alignment {
313            Alignment::Top => limits.max_height(height_above),
314            Alignment::TopStart | Alignment::TopEnd => {
315                limits.max_height((height_above + self.underlay_bounds.height).max(0.0))
316            }
317            Alignment::Bottom => limits.max_height(height_below),
318            Alignment::BottomEnd | Alignment::BottomStart => {
319                limits.max_height((height_below + self.underlay_bounds.height).max(0.0))
320            }
321            Alignment::Start | Alignment::End => limits.max_height(max_height_symmetric),
322        };
323
324        let mut node = self
325            .element
326            .as_widget_mut()
327            .layout(self.state, renderer, &limits);
328
329        let mut new_position = match self.alignment {
330            Alignment::TopStart => Point::new(
331                previous_position.x - node.bounds().width - self.offset.x,
332                previous_position.y - node.bounds().height + self.underlay_bounds.height
333                    - self.offset.y,
334            ),
335            Alignment::Top => Point::new(
336                previous_position.x + self.underlay_bounds.width / 2.0 - node.bounds().width / 2.0,
337                previous_position.y - node.bounds().height - self.offset.y,
338            ),
339            Alignment::TopEnd => Point::new(
340                previous_position.x + self.underlay_bounds.width + self.offset.x,
341                previous_position.y - node.bounds().height + self.underlay_bounds.height
342                    - self.offset.y,
343            ),
344            Alignment::End => Point::new(
345                previous_position.x + self.underlay_bounds.width + self.offset.x,
346                previous_position.y + self.underlay_bounds.height / 2.0
347                    - node.bounds().height / 2.0,
348            ),
349            Alignment::BottomEnd => Point::new(
350                previous_position.x + self.underlay_bounds.width + self.offset.x,
351                previous_position.y + self.offset.y,
352            ),
353            Alignment::Bottom => Point::new(
354                previous_position.x + self.underlay_bounds.width / 2.0 - node.bounds().width / 2.0,
355                previous_position.y + self.underlay_bounds.height + self.offset.y,
356            ),
357            Alignment::BottomStart => Point::new(
358                previous_position.x - node.bounds().width - self.offset.x,
359                previous_position.y + self.offset.y,
360            ),
361            Alignment::Start => Point::new(
362                previous_position.x - node.bounds().width - self.offset.x,
363                previous_position.y + self.underlay_bounds.height / 2.0
364                    - node.bounds().height / 2.0,
365            ),
366        };
367
368        if new_position.x + node.bounds().width > max.width {
369            new_position.x = max.width - node.bounds().width;
370        }
371        if new_position.x < 0.0 {
372            new_position.x = 0.0;
373        }
374
375        if new_position.y + node.bounds().height > max.height {
376            new_position.y = max.height - node.bounds().height;
377        }
378        if new_position.y < 0.0 {
379            new_position.y = 0.0;
380        }
381
382        node.move_to_mut(new_position);
383        node
384    }
385
386    fn draw(
387        &self,
388        renderer: &mut Renderer,
389        theme: &Theme,
390        style: &renderer::Style,
391        layout: Layout<'_>,
392        cursor: Cursor,
393    ) {
394        let bounds = layout.bounds();
395        self.element
396            .as_widget()
397            .draw(self.state, renderer, theme, style, layout, cursor, &bounds);
398    }
399
400    fn update(
401        &mut self,
402        event: &Event,
403        layout: Layout<'_>,
404        cursor: Cursor,
405        renderer: &Renderer,
406        clipboard: &mut dyn Clipboard,
407        shell: &mut Shell<Message>,
408    ) {
409        self.underlay_bounds = Rectangle {
410            x: self.position.x,
411            y: self.position.y,
412            width: self.underlay_bounds.width,
413            height: self.underlay_bounds.height,
414        };
415
416        if let Some(on_dismiss) = self.on_dismiss {
417            match &event {
418                Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
419                    if key == &keyboard::Key::Named(Named::Escape) {
420                        shell.publish(on_dismiss.clone());
421                    }
422                }
423
424                Event::Mouse(mouse::Event::ButtonPressed(
425                    mouse::Button::Left | mouse::Button::Right,
426                ))
427                | Event::Touch(touch::Event::FingerPressed { .. }) => {
428                    if !cursor.is_over(layout.bounds()) && !cursor.is_over(self.underlay_bounds) {
429                        shell.publish(on_dismiss.clone());
430                    }
431                }
432
433                _ => {}
434            }
435        }
436
437        self.element.as_widget_mut().update(
438            self.state,
439            event,
440            layout,
441            cursor,
442            renderer,
443            clipboard,
444            shell,
445            &layout.bounds(),
446        );
447    }
448
449    fn mouse_interaction(
450        &self,
451        layout: Layout<'_>,
452        cursor: Cursor,
453        renderer: &Renderer,
454    ) -> mouse::Interaction {
455        self.element.as_widget().mouse_interaction(
456            self.state,
457            layout,
458            cursor,
459            &self.viewport,
460            renderer,
461        )
462    }
463}