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