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 mut node = self.container.layout(self.state, renderer, &limits);
203
204 node = node.clone().move_to(if space_below > space_above {
205 position + Vector::new(0.0, self.target_height)
206 } else {
207 position - Vector::new(0.0, node.size().height)
208 });
209
210 node
211 }
212
213 fn on_event(
214 &mut self,
215 event: Event,
216 layout: Layout<'_>,
217 cursor: mouse::Cursor,
218 renderer: &crate::Renderer,
219 clipboard: &mut dyn Clipboard,
220 shell: &mut Shell<'_, Message>,
221 ) -> event::Status {
222 let bounds = layout.bounds();
223
224 self.container.on_event(
225 self.state, event, layout, cursor, renderer, clipboard, shell, &bounds,
226 )
227 }
228
229 fn mouse_interaction(
230 &self,
231 layout: Layout<'_>,
232 cursor: mouse::Cursor,
233 viewport: &Rectangle,
234 renderer: &crate::Renderer,
235 ) -> mouse::Interaction {
236 self.container
237 .mouse_interaction(self.state, layout, cursor, viewport, renderer)
238 }
239
240 fn draw(
241 &self,
242 renderer: &mut crate::Renderer,
243 theme: &crate::Theme,
244 style: &renderer::Style,
245 layout: Layout<'_>,
246 cursor: mouse::Cursor,
247 ) {
248 let appearance = theme.appearance(&self.style);
249 let bounds = layout.bounds();
250
251 renderer.fill_quad(
252 renderer::Quad {
253 bounds,
254 border: Border {
255 width: appearance.border_width,
256 color: appearance.border_color,
257 radius: appearance.border_radius,
258 },
259 shadow: Shadow::default(),
260 },
261 appearance.background,
262 );
263
264 self.container
265 .draw(self.state, renderer, theme, style, layout, cursor, &bounds);
266 }
267}
268
269struct InnerList<'a, S, Item, Message> {
270 options: &'a Model<S, Item>,
271 hovered_option: &'a mut Option<Item>,
272 selected_option: Option<&'a Item>,
273 on_selected: Box<dyn FnMut(Item) -> Message + 'a>,
274 on_option_hovered: Option<&'a dyn Fn(Item) -> Message>,
275 padding: Padding,
276 text_size: Option<f32>,
277 text_line_height: text::LineHeight,
278}
279
280impl<S, Item, Message> Widget<Message, crate::Theme, crate::Renderer>
281 for InnerList<'_, S, Item, Message>
282where
283 S: AsRef<str>,
284 Item: Clone + PartialEq,
285{
286 fn size(&self) -> Size<Length> {
287 Size::new(Length::Fill, Length::Shrink)
288 }
289
290 fn layout(
291 &self,
292 _tree: &mut Tree,
293 renderer: &crate::Renderer,
294 limits: &layout::Limits,
295 ) -> layout::Node {
296 use std::f32;
297
298 let limits = limits.width(Length::Fill).height(Length::Shrink);
299 let text_size = self
300 .text_size
301 .unwrap_or_else(|| text::Renderer::default_size(renderer).0);
302
303 let text_line_height = self.text_line_height.to_absolute(Pixels(text_size));
304
305 let lists = self.options.lists.len();
306 let (descriptions, options) = self.options.lists.iter().fold((0, 0), |acc, l| {
307 (
308 acc.0 + i32::from(l.description.is_some()),
309 acc.1 + l.options.len(),
310 )
311 });
312
313 let vertical_padding = self.padding.vertical();
314 let text_line_height = f32::from(text_line_height);
315
316 let size = {
317 #[allow(clippy::cast_precision_loss)]
318 let intrinsic = Size::new(0.0, {
319 let text = vertical_padding + text_line_height;
320 let separators = ((vertical_padding / 2.0) + 1.0) * (lists - 1) as f32;
321 let descriptions = (text + 4.0) * descriptions as f32;
322 let options = text * options as f32;
323 separators + descriptions + options
324 });
325
326 limits.resolve(Length::Fill, Length::Shrink, intrinsic)
327 };
328
329 layout::Node::new(size)
330 }
331
332 fn on_event(
333 &mut self,
334 _state: &mut Tree,
335 event: Event,
336 layout: Layout<'_>,
337 cursor: mouse::Cursor,
338 renderer: &crate::Renderer,
339 _clipboard: &mut dyn Clipboard,
340 shell: &mut Shell<'_, Message>,
341 _viewport: &Rectangle,
342 ) -> event::Status {
343 let bounds = layout.bounds();
344
345 match event {
346 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
347 if cursor.is_over(bounds) {
348 if let Some(item) = self.hovered_option.as_ref() {
349 shell.publish((self.on_selected)(item.clone()));
350 return event::Status::Captured;
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.vertical(), text_line_height);
366
367 let mut current_offset = 0.0;
368
369 let previous_hover_option = self.hovered_option.take();
370
371 for (element, elem_height) in self.options.elements().zip(heights) {
372 let bounds = Rectangle {
373 x: 0.0,
374 y: 0.0 + current_offset,
375 width: bounds.width,
376 height: elem_height,
377 };
378
379 if bounds.contains(cursor_position) {
380 *self.hovered_option = if let OptionElement::Option((_, item)) = element
381 {
382 if previous_hover_option.as_ref() == Some(item) {
383 previous_hover_option
384 } else {
385 if let Some(on_option_hovered) = self.on_option_hovered {
386 shell.publish(on_option_hovered(item.clone()));
387 }
388
389 Some(item.clone())
390 }
391 } else {
392 None
393 };
394
395 break;
396 }
397 current_offset += elem_height;
398 }
399 }
400 }
401 Event::Touch(touch::Event::FingerPressed { .. }) => {
402 if let Some(cursor_position) = cursor.position_in(bounds) {
403 let text_size = self
404 .text_size
405 .unwrap_or_else(|| text::Renderer::default_size(renderer).0);
406
407 let text_line_height =
408 f32::from(self.text_line_height.to_absolute(Pixels(text_size)));
409
410 let heights = self
411 .options
412 .element_heights(self.padding.vertical(), text_line_height);
413
414 let mut current_offset = 0.0;
415
416 let previous_hover_option = self.hovered_option.take();
417
418 for (element, elem_height) in self.options.elements().zip(heights) {
419 let bounds = Rectangle {
420 x: 0.0,
421 y: 0.0 + current_offset,
422 width: bounds.width,
423 height: elem_height,
424 };
425
426 if bounds.contains(cursor_position) {
427 *self.hovered_option = if let OptionElement::Option((_, item)) = element
428 {
429 if previous_hover_option.as_ref() == Some(item) {
430 previous_hover_option
431 } else {
432 Some(item.clone())
433 }
434 } else {
435 None
436 };
437
438 if let Some(item) = self.hovered_option {
439 shell.publish((self.on_selected)(item.clone()));
440 }
441
442 break;
443 }
444 current_offset += elem_height;
445 }
446 }
447 }
448 _ => {}
449 }
450
451 event::Status::Ignored
452 }
453
454 fn mouse_interaction(
455 &self,
456 _state: &Tree,
457 layout: Layout<'_>,
458 cursor: mouse::Cursor,
459 _viewport: &Rectangle,
460 _renderer: &crate::Renderer,
461 ) -> mouse::Interaction {
462 let is_mouse_over = cursor.is_over(layout.bounds());
463
464 if is_mouse_over {
465 mouse::Interaction::Pointer
466 } else {
467 mouse::Interaction::default()
468 }
469 }
470
471 #[allow(clippy::too_many_lines)]
472 fn draw(
473 &self,
474 _state: &Tree,
475 renderer: &mut crate::Renderer,
476 theme: &crate::Theme,
477 style: &renderer::Style,
478 layout: Layout<'_>,
479 cursor: mouse::Cursor,
480 viewport: &Rectangle,
481 ) {
482 let appearance = theme.appearance(&());
483 let bounds = layout.bounds();
484
485 let text_size = self
486 .text_size
487 .unwrap_or_else(|| text::Renderer::default_size(renderer).0);
488
489 let offset = viewport.y - bounds.y;
490
491 let text_line_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size)));
492
493 let visible_options = self.options.visible_options(
494 self.padding.vertical(),
495 text_line_height,
496 offset,
497 viewport.height,
498 );
499
500 let mut current_offset = 0.0;
501
502 for (elem, elem_height) in visible_options {
503 let mut bounds = Rectangle {
504 x: bounds.x,
505 y: bounds.y + current_offset,
506 width: bounds.width,
507 height: elem_height,
508 };
509
510 current_offset += elem_height;
511
512 match elem {
513 OptionElement::Option((option, item)) => {
514 let (color, font) = if self.selected_option.as_ref() == Some(&item) {
515 let item_x = bounds.x + appearance.border_width;
516 let item_width = bounds.width - appearance.border_width * 2.0;
517
518 bounds = Rectangle {
519 x: item_x,
520 width: item_width,
521 ..bounds
522 };
523
524 renderer.fill_quad(
525 renderer::Quad {
526 bounds,
527 border: Border {
528 radius: appearance.border_radius,
529 ..Default::default()
530 },
531 shadow: Shadow::default(),
532 },
533 appearance.selected_background,
534 );
535
536 let svg_handle =
537 svg::Svg::new(crate::widget::common::object_select().clone())
538 .color(appearance.selected_text_color)
539 .border_radius(appearance.border_radius);
540 svg::Renderer::draw_svg(
541 renderer,
542 svg_handle,
543 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
551 (appearance.selected_text_color, crate::font::semibold())
552 } else if self.hovered_option.as_ref() == Some(item) {
553 let item_x = bounds.x + appearance.border_width;
554 let item_width = bounds.width - appearance.border_width * 2.0;
555
556 bounds = Rectangle {
557 x: item_x,
558 width: item_width,
559 ..bounds
560 };
561
562 renderer.fill_quad(
563 renderer::Quad {
564 bounds,
565 border: Border {
566 radius: appearance.border_radius,
567 ..Default::default()
568 },
569 shadow: Shadow::default(),
570 },
571 appearance.hovered_background,
572 );
573
574 (appearance.hovered_text_color, crate::font::default())
575 } else {
576 (appearance.text_color, crate::font::default())
577 };
578
579 let bounds = Rectangle {
580 x: bounds.x + self.padding.left,
581 y: bounds.y + self.padding.top + 8.0,
583 width: bounds.width,
584 height: elem_height,
585 };
586 text::Renderer::fill_text(
587 renderer,
588 Text {
589 content: option.as_ref().to_string(),
590 bounds: bounds.size(),
591 size: iced::Pixels(text_size),
592 line_height: self.text_line_height,
593 font,
594 horizontal_alignment: alignment::Horizontal::Left,
595 vertical_alignment: alignment::Vertical::Center,
596 shaping: text::Shaping::Advanced,
597 wrapping: text::Wrapping::default(),
598 },
599 bounds.position(),
600 color,
601 *viewport,
602 );
603 }
604
605 OptionElement::Separator => {
606 let divider = crate::widget::divider::horizontal::light().height(1.0);
607
608 let layout_node = layout::Node::new(Size {
609 width: bounds.width,
610 height: 1.0,
611 })
612 .move_to(Point {
613 x: bounds.x,
614 y: bounds.y + (self.padding.vertical() / 2.0) - 4.0,
615 });
616
617 Widget::<Message, crate::Theme, crate::Renderer>::draw(
618 crate::Element::<Message>::from(divider).as_widget(),
619 &Tree::empty(),
620 renderer,
621 theme,
622 style,
623 Layout::new(&layout_node),
624 cursor,
625 viewport,
626 );
627 }
628
629 OptionElement::Description(description) => {
630 let bounds = Rectangle {
631 x: bounds.center_x(),
632 y: bounds.center_y(),
633 ..bounds
634 };
635 text::Renderer::fill_text(
636 renderer,
637 Text {
638 content: description.as_ref().to_string(),
639 bounds: bounds.size(),
640 size: iced::Pixels(text_size),
641 line_height: text::LineHeight::Absolute(Pixels(text_line_height + 4.0)),
642 font: crate::font::default(),
643 horizontal_alignment: alignment::Horizontal::Center,
644 vertical_alignment: alignment::Vertical::Center,
645 shaping: text::Shaping::Advanced,
646 wrapping: text::Wrapping::default(),
647 },
648 bounds.position(),
649 appearance.description_color,
650 *viewport,
651 );
652 }
653 }
654 }
655 }
656}
657
658impl<'a, S, Item, Message: 'a> From<InnerList<'a, S, Item, Message>>
659 for Element<'a, Message, crate::Theme, crate::Renderer>
660where
661 S: AsRef<str>,
662 Item: Clone + PartialEq,
663{
664 fn from(list: InnerList<'a, S, Item, Message>) -> Self {
665 Element::new(list)
666 }
667}
668
669pub(super) enum OptionElement<'a, S, Item> {
670 Description(&'a S),
671 Option(&'a (S, Item)),
672 Separator,
673}
674
675impl<S, Message> Model<S, Message> {
676 pub(super) fn elements(&self) -> impl Iterator<Item = OptionElement<S, Message>> + '_ {
677 let iterator = self.lists.iter().flat_map(|list| {
678 let description = list
679 .description
680 .as_ref()
681 .into_iter()
682 .map(OptionElement::Description);
683
684 let options = list.options.iter().map(OptionElement::Option);
685
686 description
687 .chain(options)
688 .chain(std::iter::once(OptionElement::Separator))
689 });
690
691 iterator
692 }
693
694 fn element_heights(
695 &self,
696 vertical_padding: f32,
697 text_line_height: f32,
698 ) -> impl Iterator<Item = f32> + '_ {
699 self.elements().map(move |element| match element {
700 OptionElement::Option(_) => vertical_padding + text_line_height,
701 OptionElement::Separator => (vertical_padding / 2.0) + 1.0,
702 OptionElement::Description(_) => vertical_padding + text_line_height + 4.0,
703 })
704 }
705
706 fn visible_options(
707 &self,
708 padding_vertical: f32,
709 text_line_height: f32,
710 offset: f32,
711 height: f32,
712 ) -> impl Iterator<Item = (OptionElement<S, Message>, f32)> + '_ {
713 let heights = self.element_heights(padding_vertical, text_line_height);
714
715 let mut current = 0.0;
716 self.elements()
717 .zip(heights)
718 .filter(move |(_, element_height)| {
719 let end = current + element_height;
720 let visible = current >= offset && end <= offset + height;
721 current = end;
722 visible
723 })
724 }
725}