1use std::{cell::Cell, rc::Rc};
2
3use iced::advanced::Clipboard;
4use iced_widget::core::{
5 Element, Event, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, keyboard,
6 layout::{Limits, Node},
7 mouse::{self, Cursor, Interaction},
8 overlay,
9 renderer::Style,
10 widget::{Operation, Tree, tree},
11};
12
13#[derive(Debug)]
14struct State {
15 position: Option<Point>,
16}
17
18#[derive(Debug, Default, Clone, Copy)]
28pub enum Placement {
29 TopLeft,
30 TopCenter,
31 TopRight,
32 RightTop,
33 RightCenter,
34 RightBottom,
35 BottomRight,
36 BottomCenter,
37 #[default]
38 BottomLeft,
39 LeftBottom,
40 LeftCenter,
41 LeftTop,
42}
43
44impl Placement {
45 #[must_use]
46 fn flip_x(self) -> Self {
47 match self {
48 Self::TopLeft => Self::TopRight,
49 Self::TopRight => Self::TopLeft,
50 Self::RightTop => Self::LeftTop,
51 Self::RightCenter => Self::LeftCenter,
52 Self::RightBottom => Self::LeftBottom,
53 Self::BottomRight => Self::BottomLeft,
54 Self::BottomLeft => Self::BottomRight,
55 Self::LeftBottom => Self::RightBottom,
56 Self::LeftCenter => Self::RightCenter,
57 Self::LeftTop => Self::RightTop,
58 Self::TopCenter | Self::BottomCenter => self,
59 }
60 }
61
62 #[must_use]
63 fn flip_y(self) -> Self {
64 match self {
65 Self::TopLeft => Self::BottomLeft,
66 Self::TopCenter => Self::BottomCenter,
67 Self::TopRight => Self::BottomRight,
68 Self::RightTop => Self::RightBottom,
69 Self::RightBottom => Self::RightTop,
70 Self::BottomRight => Self::TopRight,
71 Self::BottomCenter => Self::TopCenter,
72 Self::BottomLeft => Self::TopLeft,
73 Self::LeftBottom => Self::LeftTop,
74 Self::LeftTop => Self::LeftBottom,
75 Self::LeftCenter | Self::RightCenter => self,
76 }
77 }
78
79 fn offset(&self, bounds: &Rectangle, overlay_bounds: &Rectangle) -> Vector {
81 match self {
82 Placement::TopLeft => {
83 Vector::new(-overlay_bounds.width + bounds.width, -overlay_bounds.height)
84 }
85 Placement::TopCenter => Vector::new(
86 (-overlay_bounds.width + bounds.width) / 2.0,
87 -overlay_bounds.height,
88 ),
89 Placement::TopRight => Vector::new(0.0, -overlay_bounds.height),
90 Placement::RightCenter => {
91 Vector::new(bounds.width, (-overlay_bounds.height + bounds.height) / 2.0)
92 }
93 Placement::RightTop => {
94 Vector::new(bounds.width, -overlay_bounds.height + bounds.height)
95 }
96 Placement::RightBottom => Vector::new(bounds.width, 0.0),
97 Placement::BottomRight => Vector::new(0.0, bounds.height),
98 Placement::BottomCenter => {
99 Vector::new((-overlay_bounds.width + bounds.width) / 2.0, bounds.height)
100 }
101 Placement::BottomLeft => {
102 Vector::new(-overlay_bounds.width + bounds.width, bounds.height)
103 }
104 Placement::LeftBottom => Vector::new(-overlay_bounds.width, 0.0),
105 Placement::LeftCenter => Vector::new(
106 -overlay_bounds.width,
107 (-overlay_bounds.height + bounds.height) / 2.0,
108 ),
109 Placement::LeftTop => Vector::new(
110 -overlay_bounds.width,
111 -overlay_bounds.height + bounds.height,
112 ),
113 }
114 }
115}
116
117pub struct DropDownMenu<'a, Message, Theme, Renderer> {
118 trigger: Box<dyn Fn(bool) -> Element<'a, Message, Theme, Renderer> + 'a>,
120 menu: Option<Element<'a, Message, Theme, Renderer>>,
121 overlay_bounds: Option<Rectangle>,
122 trigger_cached: Option<Element<'a, Message, Theme, Renderer>>,
123 placement: Placement,
124 open_cached: Rc<Cell<bool>>,
125 just_closed: Rc<Cell<bool>>,
126 menu_transparent: bool,
127 trigger_transparent: bool,
128 trigger_bounds: Option<Rectangle>,
129}
130
131impl<'a, Message, Theme, Renderer> DropDownMenu<'a, Message, Theme, Renderer> {
132 pub fn new(
133 trigger: impl Fn(bool) -> Element<'a, Message, Theme, Renderer> + 'a,
134 menu: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
135 placement: Placement,
136 ) -> Self {
137 Self {
138 trigger: Box::new(trigger),
139 menu: menu.map(|e| e.into()),
140 overlay_bounds: None,
141 trigger_cached: None,
142 placement,
143 open_cached: Rc::new(Cell::new(false)),
144 just_closed: Rc::new(Cell::new(false)),
145 menu_transparent: false,
146 trigger_transparent: false,
147 trigger_bounds: None,
148 }
149 }
150
151 pub fn menu_transparent(mut self, transparent: bool) -> Self {
152 self.menu_transparent = transparent;
153 self
154 }
155 pub fn trigger_transparent(mut self, transparent: bool) -> Self {
156 self.trigger_transparent = transparent;
157 self
158 }
159}
160
161impl<Message, Theme, Renderer: iced::advanced::Renderer> Widget<Message, Theme, Renderer>
162 for DropDownMenu<'_, Message, Theme, Renderer>
163{
164 fn size(&self) -> Size<Length> {
165 (self.trigger)(self.open_cached.get()).as_widget().size()
166 }
167
168 fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
169 if let Some(menu) = &mut self.menu {
170 let overlay_bounds = menu
171 .as_widget_mut()
172 .layout(&mut tree.children[1], renderer, &Limits::NONE)
173 .bounds();
174 self.overlay_bounds = Some(overlay_bounds);
175 }
176 self.trigger_cached = Some((self.trigger)(
177 tree.state.downcast_ref::<State>().position.is_some(),
178 ));
179 let trigger = self
180 .trigger_cached
181 .as_mut()
182 .unwrap()
183 .as_widget_mut()
184 .layout(&mut tree.children[0], renderer, limits);
185 self.trigger_bounds = Some(trigger.bounds());
186 trigger
187 }
188
189 fn draw(
190 &self,
191 tree: &Tree,
192 renderer: &mut Renderer,
193 theme: &Theme,
194 style: &Style,
195 layout: Layout<'_>,
196 cursor: Cursor,
197 viewport: &Rectangle,
198 ) {
199 (self.trigger)(tree.state.downcast_ref::<State>().position.is_some())
200 .as_widget()
201 .draw(
202 &tree.children[0],
203 renderer,
204 theme,
205 style,
206 layout,
207 cursor,
208 viewport,
209 );
210 }
211
212 fn tag(&self) -> tree::Tag {
213 tree::Tag::of::<State>()
214 }
215
216 fn state(&self) -> tree::State {
217 tree::State::new(State { position: None })
218 }
219
220 fn children(&self) -> Vec<Tree> {
221 if let Some(menu) = &self.menu {
222 vec![
223 Tree::new((self.trigger)(self.open_cached.get())),
224 Tree::new(menu),
225 ]
226 } else {
227 vec![Tree::new((self.trigger)(self.open_cached.get()))]
228 }
229 }
230
231 fn diff(&self, tree: &mut Tree) {
232 if let Some(menu) = &self.menu {
233 tree.diff_children(&[
234 &(self.trigger)(tree.state.downcast_ref::<State>().position.is_some()),
235 menu,
236 ]);
237 } else {
238 tree.diff_children(&[&(self.trigger)(
239 tree.state.downcast_ref::<State>().position.is_some(),
240 )]);
241 }
242 }
243
244 fn operate(
245 &mut self,
246 tree: &mut Tree,
247 layout: Layout<'_>,
248 renderer: &Renderer,
249 operation: &mut dyn Operation,
250 ) {
251 (self.trigger)(tree.state.downcast_ref::<State>().position.is_some())
252 .as_widget_mut()
253 .operate(&mut tree.children[0], layout, renderer, operation);
254 }
255
256 fn update(
257 &mut self,
258 tree: &mut Tree,
259 event: &Event,
260 layout: Layout<'_>,
261 cursor: Cursor,
262 renderer: &Renderer,
263 clipboard: &mut dyn Clipboard,
264 shell: &mut Shell<'_, Message>,
265 viewport: &Rectangle,
266 ) {
267 (self.trigger)(tree.state.downcast_ref::<State>().position.is_some())
268 .as_widget_mut()
269 .update(
270 &mut tree.children[0],
271 event,
272 layout,
273 cursor,
274 renderer,
275 clipboard,
276 shell,
277 viewport,
278 );
279
280 if self.menu.is_none() {
281 return;
282 }
283
284 if shell.is_event_captured() {
285 self.just_closed.set(false);
286 return;
287 }
288
289 let state = tree.state.downcast_mut::<State>();
290 if let Some(pos) = cursor.position()
291 && layout.bounds().contains(pos)
292 {
293 if !self.trigger_transparent
294 && let Event::Mouse(_) = event
295 {
296 shell.capture_event();
297 }
298
299 if let Event::Mouse(mouse::Event::ButtonPressed(..)) = event {
300 if self.just_closed.get() {
301 state.position = None;
302 self.open_cached.set(false);
303 } else {
304 state.position = Some(layout.bounds().position());
305 self.open_cached.set(true);
306 shell.invalidate_widgets();
307 }
308 shell.request_redraw();
309 self.just_closed.set(false);
310 shell.capture_event();
311 }
312 }
313
314 if state.position.is_some() {
315 state.position = Some(layout.bounds().position());
316 }
317 }
318
319 fn mouse_interaction(
320 &self,
321 tree: &Tree,
322 layout: Layout<'_>,
323 cursor: Cursor,
324 viewport: &Rectangle,
325 renderer: &Renderer,
326 ) -> Interaction {
327 let interaction = (self.trigger)(self.open_cached.get())
328 .as_widget()
329 .mouse_interaction(&tree.children[0], layout, cursor, viewport, renderer);
330
331 if interaction == Interaction::None
332 && cursor.is_over(layout.bounds())
333 && self.menu.is_some()
334 {
335 Interaction::Pointer
336 } else {
337 interaction
338 }
339 }
340
341 fn overlay<'a>(
342 &'a mut self,
343 tree: &'a mut Tree,
344 layout: Layout<'a>,
345 renderer: &Renderer,
346 viewport: &Rectangle,
347 translation: Vector,
348 ) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
349 let state = tree.state.downcast_mut::<State>();
350
351 let children = if let Some(menu) = &mut self.menu {
352 let [first, second] = &mut *tree.children else {
353 unreachable!();
354 };
355 [
356 self.trigger_cached.as_mut().and_then(|trigger_cached| {
359 trigger_cached.as_widget_mut().overlay(
360 first,
361 layout,
362 renderer,
363 viewport,
364 translation,
365 )
366 }),
367 state.position.map(|position| {
368 overlay::Element::new(Box::new(Overlay {
369 menu,
370 tree: second,
371 state,
372 position: position + translation,
373 open_cached: self.open_cached.clone(),
374 just_closed: self.just_closed.clone(),
375 transparent: &self.menu_transparent,
376 placement: self.placement,
377 trigger_bounds: self.trigger_bounds.unwrap(),
378 }))
379 }),
380 ]
381 .into_iter()
382 .flatten()
383 .collect::<Vec<_>>()
384 } else {
385 vec![]
386 };
387
388 (!children.is_empty()).then(|| overlay::Group::with_children(children).overlay())
389 }
390}
391
392impl<'a, Message: 'a, Theme: 'a, Renderer: iced::advanced::Renderer + 'a>
393 From<DropDownMenu<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer>
394{
395 fn from(value: DropDownMenu<'a, Message, Theme, Renderer>) -> Self {
396 Self::new(value)
397 }
398}
399
400struct Overlay<'a, 'b, Message, Theme, Renderer> {
401 menu: &'b mut Element<'a, Message, Theme, Renderer>,
402 tree: &'b mut Tree,
403 state: &'b mut State,
404 position: Point,
405 placement: Placement,
406 open_cached: Rc<Cell<bool>>,
407 just_closed: Rc<Cell<bool>>,
408 transparent: &'b bool,
409 trigger_bounds: Rectangle,
410}
411
412impl<Message, Theme, Renderer: iced::advanced::Renderer> overlay::Overlay<Message, Theme, Renderer>
418 for Overlay<'_, '_, Message, Theme, Renderer>
419{
420 fn layout(&mut self, renderer: &Renderer, bounds: Size) -> Node {
421 let layout = self
422 .menu
423 .as_widget_mut()
424 .layout(self.tree, renderer, &Limits::new(Size::ZERO, bounds))
425 .move_to(self.position);
426
427 let overlay_bounds = layout.bounds();
428 let offset = self.placement.offset(&self.trigger_bounds, &overlay_bounds);
429
430 let over_right = overlay_bounds.width + overlay_bounds.x + offset.x > bounds.width;
431 let over_left = overlay_bounds.x + offset.x < 0.0;
432
433 let placement_flipped = self.placement.flip_x();
434 let offset_flipped = placement_flipped.offset(&self.trigger_bounds, &overlay_bounds);
435 let flipped_fits_right =
436 overlay_bounds.width + overlay_bounds.x + offset_flipped.x < bounds.width;
437 let flipped_fits_left = overlay_bounds.x + offset_flipped.x > 0.0;
438
439 if (over_right && flipped_fits_left) || (over_left && flipped_fits_right) {
440 self.placement = placement_flipped;
441 }
442
443 let over_top = overlay_bounds.y + offset.y < 0.0;
444 let over_bottom = overlay_bounds.height + overlay_bounds.y + offset.y > bounds.height;
445
446 let placement_flipped = self.placement.flip_y();
447 let offset_flipped = placement_flipped.offset(&self.trigger_bounds, &overlay_bounds);
448 let flipped_fits_top = overlay_bounds.y + offset_flipped.y > 0.0;
449 let flipped_fits_bottom =
450 overlay_bounds.height + overlay_bounds.y + offset_flipped.y < bounds.height;
451
452 if (over_top && flipped_fits_bottom) || (over_bottom && flipped_fits_top) {
453 self.placement = placement_flipped;
454 }
455
456 self.menu
457 .as_widget_mut()
458 .layout(self.tree, renderer, &Limits::new(Size::ZERO, bounds))
459 .move_to(self.position + self.placement.offset(&self.trigger_bounds, &overlay_bounds))
460 }
461
462 fn draw(
463 &self,
464 renderer: &mut Renderer,
465 theme: &Theme,
466 style: &Style,
467 layout: Layout<'_>,
468 cursor: Cursor,
469 ) {
470 renderer.with_layer(Rectangle::INFINITE, |renderer| {
471 self.menu.as_widget().draw(
472 self.tree,
473 renderer,
474 theme,
475 style,
476 layout,
477 cursor,
478 &layout.bounds(),
479 );
480 });
481 }
482
483 fn operate(&mut self, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation) {
484 self.menu
485 .as_widget_mut()
486 .operate(self.tree, layout, renderer, operation);
487 }
488
489 fn update(
490 &mut self,
491 event: &Event,
492 layout: Layout<'_>,
493 cursor: Cursor,
494 renderer: &Renderer,
495 clipboard: &mut dyn Clipboard,
496 shell: &mut Shell<'_, Message>,
497 ) {
498 let was_event_captured = shell.is_event_captured();
499
500 self.menu.as_widget_mut().update(
501 self.tree,
502 event,
503 layout,
504 cursor,
505 renderer,
506 clipboard,
507 shell,
508 &layout.bounds(),
509 );
510
511 if was_event_captured {
512 return;
513 }
514
515 match event {
516 Event::Mouse(mouse::Event::ButtonPressed { .. }) => {
517 if !cursor.is_over(layout.bounds()) {
518 self.state.position = None;
519 self.just_closed.set(true);
520 self.open_cached.set(false);
521 shell.invalidate_widgets();
522 shell.request_redraw();
523 }
524 }
525 Event::Mouse(mouse::Event::ButtonReleased { .. })
526 if shell.is_event_captured() && cursor.is_over(layout.bounds()) =>
527 {
528 self.state.position = None;
529 self.just_closed.set(true);
530 self.open_cached.set(false);
531 shell.invalidate_widgets();
532 shell.request_redraw();
533 }
534 Event::Keyboard(keyboard::Event::KeyPressed {
535 key: keyboard::Key::Named(keyboard::key::Named::Escape),
536 ..
537 }) => {
538 self.state.position = None;
539 self.just_closed.set(true);
540 self.open_cached.set(false);
541 shell.invalidate_widgets();
542 shell.request_redraw();
543 }
544 _ => {}
545 }
546
547 if let Event::Mouse(_) = event
548 && !*self.transparent
549 && cursor.is_over(layout.bounds())
550 {
551 shell.capture_event();
552 }
553 }
554
555 fn mouse_interaction(
556 &self,
557 layout: Layout<'_>,
558 cursor: Cursor,
559 renderer: &Renderer,
560 ) -> Interaction {
561 let interaction = self.menu.as_widget().mouse_interaction(
562 self.tree,
563 layout,
564 cursor,
565 &layout.bounds(),
566 renderer,
567 );
568
569 if interaction == Interaction::None && cursor.is_over(layout.bounds()) {
570 Interaction::Idle
571 } else {
572 interaction
573 }
574 }
575
576 fn overlay<'a>(
577 &'a mut self,
578 layout: Layout<'a>,
579 renderer: &Renderer,
580 ) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
581 self.menu.as_widget_mut().overlay(
582 self.tree,
583 layout,
584 renderer,
585 &layout.bounds(),
586 Vector::ZERO,
587 )
588 }
589}