1use super::Model;
2pub use crate::widget::dropdown::menu::{Appearance, StyleSheet};
3
4use crate::widget::Container;
5use iced_core::event::{self, Event};
6use iced_core::layout::{self, Layout};
7use iced_core::text::{self, Text};
8use iced_core::widget::Tree;
9use iced_core::{
10 Border, Clipboard, Element, Length, Padding, Pixels, Point, Rectangle, Renderer, Shadow, Shell,
11 Size, Vector, Widget, alignment, mouse, overlay, renderer, svg, touch,
12};
13use iced_widget::scrollable::Scrollable;
14
15#[must_use]
17pub struct Menu<'a, S, Item, Message>
18where
19 S: AsRef<str>,
20{
21 state: &'a mut State,
22 options: &'a Model<S, Item>,
23 hovered_option: &'a mut Option<Item>,
24 selected_option: Option<&'a Item>,
25 on_selected: Box<dyn FnMut(Item) -> Message + 'a>,
26 on_option_hovered: Option<&'a dyn Fn(Item) -> Message>,
27 width: f32,
28 padding: Padding,
29 text_size: Option<f32>,
30 text_line_height: text::LineHeight,
31 style: (),
32}
33
34impl<'a, S, Item, Message: 'a> Menu<'a, S, Item, Message>
35where
36 S: AsRef<str>,
37 Item: Clone + PartialEq,
38{
39 pub(super) fn new(
42 state: &'a mut State,
43 options: &'a Model<S, Item>,
44 hovered_option: &'a mut Option<Item>,
45 selected_option: Option<&'a Item>,
46 on_selected: impl FnMut(Item) -> Message + 'a,
47 on_option_hovered: Option<&'a dyn Fn(Item) -> Message>,
48 ) -> Self {
49 Menu {
50 state,
51 options,
52 hovered_option,
53 selected_option,
54 on_selected: Box::new(on_selected),
55 on_option_hovered,
56 width: 0.0,
57 padding: Padding::ZERO,
58 text_size: None,
59 text_line_height: text::LineHeight::Absolute(Pixels::from(16.0)),
60 style: Default::default(),
61 }
62 }
63
64 pub fn width(mut self, width: f32) -> Self {
66 self.width = width;
67 self
68 }
69
70 pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
72 self.padding = padding.into();
73 self
74 }
75
76 pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
78 self.text_size = Some(text_size.into().0);
79 self
80 }
81
82 pub fn text_line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
84 self.text_line_height = line_height.into();
85 self
86 }
87
88 #[must_use]
95 pub fn overlay(
96 self,
97 position: Point,
98 target_height: f32,
99 ) -> overlay::Element<'a, Message, crate::Theme, crate::Renderer> {
100 overlay::Element::new(Box::new(Overlay::new(self, target_height, position)))
101 }
102}
103
104#[must_use]
106#[derive(Debug)]
107pub(super) struct State {
108 tree: Tree,
109}
110
111impl State {
112 pub fn new() -> Self {
114 Self {
115 tree: Tree::empty(),
116 }
117 }
118}
119
120impl Default for State {
121 fn default() -> Self {
122 Self::new()
123 }
124}
125
126struct Overlay<'a, Message> {
127 state: &'a mut Tree,
128 container: Container<'a, Message, crate::Theme, crate::Renderer>,
129 width: f32,
130 target_height: f32,
131 style: (),
132 position: Point,
133}
134
135impl<'a, Message: 'a> Overlay<'a, Message> {
136 pub fn new<S: AsRef<str>, Item: Clone + PartialEq>(
137 menu: Menu<'a, S, Item, Message>,
138 target_height: f32,
139 position: Point,
140 ) -> Self {
141 let Menu {
142 state,
143 options,
144 hovered_option,
145 selected_option,
146 on_selected,
147 on_option_hovered,
148 width,
149 padding,
150 text_size,
151 text_line_height,
152 style,
153 } = menu;
154
155 let mut container = Container::new(Scrollable::new(
156 Container::new(InnerList {
157 options,
158 hovered_option,
159 selected_option,
160 on_selected,
161 on_option_hovered,
162 padding,
163 text_size,
164 text_line_height,
165 })
166 .padding(padding),
167 ))
168 .class(crate::style::Container::Dropdown);
169
170 state.tree.diff(&mut container as &mut dyn Widget<_, _, _>);
171
172 Self {
173 state: &mut state.tree,
174 container,
175 width,
176 target_height,
177 style,
178 position,
179 }
180 }
181}
182
183impl<Message> iced_core::Overlay<Message, crate::Theme, crate::Renderer> for Overlay<'_, Message> {
184 fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node {
185 let position = self.position;
186 let space_below = bounds.height - (position.y + self.target_height);
187 let space_above = position.y;
188
189 let limits = layout::Limits::new(
190 Size::ZERO,
191 Size::new(
192 bounds.width - position.x,
193 if space_below > space_above {
194 space_below
195 } else {
196 space_above
197 },
198 ),
199 )
200 .width(self.width);
201
202 let node = self.container.layout(self.state, renderer, &limits);
203
204 let node_size = node.size();
205 node.move_to(if space_below > space_above {
206 position + Vector::new(0.0, self.target_height)
207 } else {
208 position - Vector::new(0.0, node_size.height)
209 })
210 }
211
212 fn update(
213 &mut self,
214 event: &Event,
215 layout: Layout<'_>,
216 cursor: mouse::Cursor,
217 renderer: &crate::Renderer,
218 clipboard: &mut dyn Clipboard,
219 shell: &mut Shell<'_, Message>,
220 ) {
221 let bounds = layout.bounds();
222
223 self.container.update(
224 self.state, event, layout, cursor, renderer, clipboard, shell, &bounds,
225 )
226 }
227
228 fn mouse_interaction(
229 &self,
230 layout: Layout<'_>,
231 cursor: mouse::Cursor,
232 renderer: &crate::Renderer,
233 ) -> mouse::Interaction {
234 self.container
235 .mouse_interaction(self.state, layout, cursor, &layout.bounds(), renderer)
236 }
237
238 fn draw(
239 &self,
240 renderer: &mut crate::Renderer,
241 theme: &crate::Theme,
242 style: &renderer::Style,
243 layout: Layout<'_>,
244 cursor: mouse::Cursor,
245 ) {
246 let appearance = theme.appearance(&self.style);
247 let bounds = layout.bounds();
248
249 renderer.fill_quad(
250 renderer::Quad {
251 bounds,
252 border: Border {
253 width: appearance.border_width,
254 color: appearance.border_color,
255 radius: appearance.border_radius,
256 },
257 shadow: Shadow::default(),
258 snap: true,
259 },
260 appearance.background,
261 );
262
263 self.container
264 .draw(self.state, renderer, theme, style, layout, cursor, &bounds);
265 }
266}
267
268struct InnerList<'a, S, Item, Message> {
269 options: &'a Model<S, Item>,
270 hovered_option: &'a mut Option<Item>,
271 selected_option: Option<&'a Item>,
272 on_selected: Box<dyn FnMut(Item) -> Message + 'a>,
273 on_option_hovered: Option<&'a dyn Fn(Item) -> Message>,
274 padding: Padding,
275 text_size: Option<f32>,
276 text_line_height: text::LineHeight,
277}
278
279impl<S, Item, Message> Widget<Message, crate::Theme, crate::Renderer>
280 for InnerList<'_, S, Item, Message>
281where
282 S: AsRef<str>,
283 Item: Clone + PartialEq,
284{
285 fn size(&self) -> Size<Length> {
286 Size::new(Length::Fill, Length::Shrink)
287 }
288
289 fn layout(
290 &mut self,
291 _tree: &mut Tree,
292 renderer: &crate::Renderer,
293 limits: &layout::Limits,
294 ) -> layout::Node {
295 use std::f32;
296
297 let limits = limits.width(Length::Fill).height(Length::Shrink);
298 let text_size = self
299 .text_size
300 .unwrap_or_else(|| text::Renderer::default_size(renderer).0);
301
302 let text_line_height = self.text_line_height.to_absolute(Pixels(text_size));
303
304 let lists = self.options.lists.len();
305 let (descriptions, options) = self.options.lists.iter().fold((0, 0), |acc, l| {
306 (
307 acc.0 + i32::from(l.description.is_some()),
308 acc.1 + l.options.len(),
309 )
310 });
311
312 let vertical_padding = self.padding.y();
313 let text_line_height = f32::from(text_line_height);
314
315 let size = {
316 #[allow(clippy::cast_precision_loss)]
317 let intrinsic = Size::new(0.0, {
318 let text = vertical_padding + text_line_height;
319 let separators = ((vertical_padding / 2.0) + 1.0) * (lists - 1) as f32;
320 let descriptions = (text + 4.0) * descriptions as f32;
321 let options = text * options as f32;
322 separators + descriptions + options
323 });
324
325 limits.resolve(Length::Fill, Length::Shrink, intrinsic)
326 };
327
328 layout::Node::new(size)
329 }
330
331 fn update(
332 &mut self,
333 _state: &mut Tree,
334 event: &Event,
335 layout: Layout<'_>,
336 cursor: mouse::Cursor,
337 renderer: &crate::Renderer,
338 _clipboard: &mut dyn Clipboard,
339 shell: &mut Shell<'_, Message>,
340 _viewport: &Rectangle,
341 ) {
342 let bounds = layout.bounds();
343
344 match event {
345 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
346 if cursor.is_over(bounds) {
347 if let Some(item) = self.hovered_option.as_ref() {
348 shell.publish((self.on_selected)(item.clone()));
349 shell.capture_event();
350 return;
351 }
352 }
353 }
354 Event::Mouse(mouse::Event::CursorMoved { .. }) => {
355 if let Some(cursor_position) = cursor.position_in(bounds) {
356 let text_size = self
357 .text_size
358 .unwrap_or_else(|| text::Renderer::default_size(renderer).0);
359
360 let text_line_height =
361 f32::from(self.text_line_height.to_absolute(Pixels(text_size)));
362
363 let heights = self
364 .options
365 .element_heights(self.padding.y(), text_line_height);
366
367 let mut current_offset = 0.0;
368
369 let previous_hover_option = self.hovered_option.clone();
370 *self.hovered_option = None;
371
372 for (element, elem_height) in self.options.elements().zip(heights) {
373 let bounds = Rectangle {
374 x: 0.0,
375 y: 0.0 + current_offset,
376 width: bounds.width,
377 height: elem_height,
378 };
379
380 if bounds.contains(cursor_position) {
381 if let OptionElement::Option((_, item)) = element {
382 *self.hovered_option = Some(item.clone());
383 if previous_hover_option.as_ref() != Some(item) {
384 if let Some(on_option_hovered) = self.on_option_hovered {
385 shell.publish(on_option_hovered(item.clone()));
386 }
387 }
388 }
389
390 break;
391 }
392 current_offset += elem_height;
393 }
394
395 if *self.hovered_option != previous_hover_option {
396 shell.request_redraw();
397 }
398 } else if self.hovered_option.is_some() {
399 *self.hovered_option = None;
400 shell.request_redraw();
401 }
402 }
403 Event::Mouse(mouse::Event::CursorLeft) => {
404 if self.hovered_option.is_some() {
405 *self.hovered_option = None;
406 shell.request_redraw();
407 }
408 }
409 Event::Touch(touch::Event::FingerPressed { .. }) => {
410 if let Some(cursor_position) = cursor.position_in(bounds) {
411 let text_size = self
412 .text_size
413 .unwrap_or_else(|| text::Renderer::default_size(renderer).0);
414
415 let text_line_height =
416 f32::from(self.text_line_height.to_absolute(Pixels(text_size)));
417
418 let heights = self
419 .options
420 .element_heights(self.padding.y(), text_line_height);
421
422 let mut current_offset = 0.0;
423
424 let previous_hover_option = self.hovered_option.take();
425
426 for (element, elem_height) in self.options.elements().zip(heights) {
427 let bounds = Rectangle {
428 x: 0.0,
429 y: 0.0 + current_offset,
430 width: bounds.width,
431 height: elem_height,
432 };
433
434 if bounds.contains(cursor_position) {
435 *self.hovered_option = if let OptionElement::Option((_, item)) = element
436 {
437 if previous_hover_option.as_ref() == Some(item) {
438 previous_hover_option
439 } else {
440 Some(item.clone())
441 }
442 } else {
443 None
444 };
445
446 if let Some(item) = self.hovered_option {
447 shell.publish((self.on_selected)(item.clone()));
448 }
449
450 break;
451 }
452 current_offset += elem_height;
453 }
454 }
455 }
456 _ => {}
457 }
458 }
459
460 fn mouse_interaction(
461 &self,
462 _state: &Tree,
463 layout: Layout<'_>,
464 cursor: mouse::Cursor,
465 _viewport: &Rectangle,
466 _renderer: &crate::Renderer,
467 ) -> mouse::Interaction {
468 let is_mouse_over = cursor.is_over(layout.bounds());
469
470 if is_mouse_over {
471 mouse::Interaction::Pointer
472 } else {
473 mouse::Interaction::default()
474 }
475 }
476
477 #[allow(clippy::too_many_lines)]
478 fn draw(
479 &self,
480 _state: &Tree,
481 renderer: &mut crate::Renderer,
482 theme: &crate::Theme,
483 style: &renderer::Style,
484 layout: Layout<'_>,
485 cursor: mouse::Cursor,
486 viewport: &Rectangle,
487 ) {
488 let appearance = theme.appearance(&());
489 let bounds = layout.bounds();
490
491 let text_size = self
492 .text_size
493 .unwrap_or_else(|| text::Renderer::default_size(renderer).0);
494
495 let offset = viewport.y - bounds.y;
496
497 let text_line_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size)));
498
499 let visible_options = self.options.visible_options(
500 self.padding.y(),
501 text_line_height,
502 offset,
503 viewport.height,
504 );
505
506 let mut current_offset = 0.0;
507
508 for (elem, elem_height) in visible_options {
509 let mut bounds = Rectangle {
510 x: bounds.x,
511 y: bounds.y + current_offset,
512 width: bounds.width,
513 height: elem_height,
514 };
515
516 current_offset += elem_height;
517
518 match elem {
519 OptionElement::Option((option, item)) => {
520 let (color, font) = if self.selected_option.as_ref() == Some(&item) {
521 let item_x = bounds.x + appearance.border_width;
522 let item_width = appearance.border_width.mul_add(-2.0, bounds.width);
523
524 bounds = Rectangle {
525 x: item_x,
526 width: item_width,
527 ..bounds
528 };
529
530 renderer.fill_quad(
531 renderer::Quad {
532 bounds,
533 border: Border {
534 radius: appearance.border_radius,
535 ..Default::default()
536 },
537 shadow: Shadow::default(),
538 snap: true,
539 },
540 appearance.selected_background,
541 );
542
543 let svg_bounds = Rectangle {
544 x: item_x + item_width - 16.0 - 8.0,
545 y: bounds.y + (bounds.height / 2.0 - 8.0),
546 width: 16.0,
547 height: 16.0,
548 };
549
550 let svg_handle =
551 svg::Svg::new(crate::widget::common::object_select().clone())
552 .color(appearance.selected_text_color)
553 .border_radius(appearance.border_radius);
554 svg::Renderer::draw_svg(renderer, svg_handle, svg_bounds, svg_bounds);
555
556 (appearance.selected_text_color, crate::font::semibold())
557 } else if self.hovered_option.as_ref() == Some(item) {
558 let item_x = bounds.x + appearance.border_width;
559 let item_width = appearance.border_width.mul_add(-2.0, bounds.width);
560
561 bounds = Rectangle {
562 x: item_x,
563 width: item_width,
564 ..bounds
565 };
566
567 renderer.fill_quad(
568 renderer::Quad {
569 bounds,
570 border: Border {
571 radius: appearance.border_radius,
572 ..Default::default()
573 },
574 shadow: Shadow::default(),
575 snap: true,
576 },
577 appearance.hovered_background,
578 );
579
580 (appearance.hovered_text_color, crate::font::default())
581 } else {
582 (appearance.text_color, crate::font::default())
583 };
584
585 let bounds = Rectangle {
586 x: bounds.x + self.padding.left,
587 y: bounds.y + self.padding.top + 8.0,
589 width: bounds.width,
590 height: elem_height,
591 };
592 text::Renderer::fill_text(
593 renderer,
594 Text {
595 content: option.as_ref().to_string(),
596 bounds: bounds.size(),
597 size: iced::Pixels(text_size),
598 line_height: self.text_line_height,
599 font,
600 align_x: text::Alignment::Left,
601 align_y: alignment::Vertical::Center,
602 shaping: text::Shaping::Advanced,
603 wrapping: text::Wrapping::default(),
604 ellipsize: text::Ellipsize::default(),
605 },
606 bounds.position(),
607 color,
608 *viewport,
609 );
610 }
611
612 OptionElement::Separator => {
613 let divider = crate::widget::divider::horizontal::light().height(1.0);
614
615 let layout_node = layout::Node::new(Size {
616 width: bounds.width,
617 height: 1.0,
618 })
619 .move_to(Point {
620 x: bounds.x,
621 y: bounds.y + (self.padding.y() / 2.0) - 4.0,
622 });
623
624 Widget::<Message, crate::Theme, crate::Renderer>::draw(
625 crate::Element::<Message>::from(divider).as_widget(),
626 &Tree::empty(),
627 renderer,
628 theme,
629 style,
630 Layout::new(&layout_node),
631 cursor,
632 viewport,
633 );
634 }
635
636 OptionElement::Description(description) => {
637 let bounds = Rectangle {
638 x: bounds.center_x(),
639 y: bounds.center_y(),
640 ..bounds
641 };
642 text::Renderer::fill_text(
643 renderer,
644 Text {
645 content: description.as_ref().to_string(),
646 bounds: bounds.size(),
647 size: iced::Pixels(text_size),
648 line_height: text::LineHeight::Absolute(Pixels(text_line_height + 4.0)),
649 font: crate::font::default(),
650 align_x: text::Alignment::Center,
651 align_y: alignment::Vertical::Center,
652 shaping: text::Shaping::Advanced,
653 wrapping: text::Wrapping::default(),
654 ellipsize: text::Ellipsize::default(),
655 },
656 bounds.position(),
657 appearance.description_color,
658 *viewport,
659 );
660 }
661 }
662 }
663 }
664}
665
666impl<'a, S, Item, Message: 'a> From<InnerList<'a, S, Item, Message>>
667 for Element<'a, Message, crate::Theme, crate::Renderer>
668where
669 S: AsRef<str>,
670 Item: Clone + PartialEq,
671{
672 fn from(list: InnerList<'a, S, Item, Message>) -> Self {
673 Element::new(list)
674 }
675}
676
677pub(super) enum OptionElement<'a, S, Item> {
678 Description(&'a S),
679 Option(&'a (S, Item)),
680 Separator,
681}
682
683impl<S, Message> Model<S, Message> {
684 pub(super) fn elements(&self) -> impl Iterator<Item = OptionElement<'_, S, Message>> + '_ {
685 self.lists.iter().flat_map(|list| {
686 let description = list
687 .description
688 .as_ref()
689 .into_iter()
690 .map(OptionElement::Description);
691
692 let options = list.options.iter().map(OptionElement::Option);
693
694 description
695 .chain(options)
696 .chain(std::iter::once(OptionElement::Separator))
697 })
698 }
699
700 fn element_heights(
701 &self,
702 vertical_padding: f32,
703 text_line_height: f32,
704 ) -> impl Iterator<Item = f32> + '_ {
705 self.elements().map(move |element| match element {
706 OptionElement::Option(_) => vertical_padding + text_line_height,
707 OptionElement::Separator => (vertical_padding / 2.0) + 1.0,
708 OptionElement::Description(_) => vertical_padding + text_line_height + 4.0,
709 })
710 }
711
712 fn visible_options(
713 &self,
714 padding_vertical: f32,
715 text_line_height: f32,
716 offset: f32,
717 height: f32,
718 ) -> impl Iterator<Item = (OptionElement<'_, S, Message>, f32)> + '_ {
719 let heights = self.element_heights(padding_vertical, text_line_height);
720
721 let mut current = 0.0;
722 self.elements()
723 .zip(heights)
724 .filter(move |(_, element_height)| {
725 let end = current + element_height;
726 let visible = current >= offset && end <= offset + height;
727 current = end;
728 visible
729 })
730 }
731}