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,
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 let node_size = node.size();
238 node.move_to(if space_below > space_above {
239 self.position + Vector::new(0.0, self.target_height)
240 } else {
241 self.position - Vector::new(0.0, node_size.height)
242 })
243 }
244
245 fn _on_event(
246 &mut self,
247 event: Event,
248 layout: Layout<'_>,
249 cursor: mouse::Cursor,
250 renderer: &crate::Renderer,
251 clipboard: &mut dyn Clipboard,
252 shell: &mut Shell<'_, Message>,
253 ) -> event::Status {
254 let bounds = layout.bounds();
255
256 self.state.with_data_mut(|tree| {
257 self.container.on_event(
258 tree, event, layout, cursor, renderer, clipboard, shell, &bounds,
259 )
260 })
261 }
262
263 fn _mouse_interaction(
264 &self,
265 layout: Layout<'_>,
266 cursor: mouse::Cursor,
267 viewport: &Rectangle,
268 renderer: &crate::Renderer,
269 ) -> mouse::Interaction {
270 self.state.with_data(|tree| {
271 self.container
272 .mouse_interaction(tree, layout, cursor, viewport, renderer)
273 })
274 }
275
276 fn _draw(
277 &self,
278 renderer: &mut crate::Renderer,
279 theme: &crate::Theme,
280 style: &renderer::Style,
281 layout: Layout<'_>,
282 cursor: mouse::Cursor,
283 ) {
284 let appearance = theme.appearance(&self.style);
285 let bounds = layout.bounds();
286
287 renderer.fill_quad(
288 renderer::Quad {
289 bounds,
290 border: Border {
291 width: appearance.border_width,
292 color: appearance.border_color,
293 radius: appearance.border_radius,
294 },
295 shadow: Shadow::default(),
296 },
297 appearance.background,
298 );
299
300 self.state.with_data(|tree| {
301 self.container
302 .draw(tree, renderer, theme, style, layout, cursor, &bounds)
303 })
304 }
305}
306
307impl<'a, Message: Clone + 'a> iced_core::Overlay<Message, crate::Theme, crate::Renderer>
308 for Overlay<'a, Message>
309{
310 fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node {
311 self._layout(renderer, bounds)
312 }
313
314 fn on_event(
315 &mut self,
316 event: Event,
317 layout: Layout<'_>,
318 cursor: mouse::Cursor,
319 renderer: &crate::Renderer,
320 clipboard: &mut dyn Clipboard,
321 shell: &mut Shell<'_, Message>,
322 ) -> event::Status {
323 self._on_event(event, layout, cursor, renderer, clipboard, shell)
324 }
325
326 fn mouse_interaction(
327 &self,
328 layout: Layout<'_>,
329 cursor: mouse::Cursor,
330 viewport: &Rectangle,
331 renderer: &crate::Renderer,
332 ) -> mouse::Interaction {
333 self._mouse_interaction(layout, cursor, viewport, renderer)
334 }
335
336 fn draw(
337 &self,
338 renderer: &mut crate::Renderer,
339 theme: &crate::Theme,
340 style: &renderer::Style,
341 layout: Layout<'_>,
342 cursor: mouse::Cursor,
343 ) {
344 self._draw(renderer, theme, style, layout, cursor);
345 }
346}
347
348impl<'a, Message: Clone + 'a> crate::widget::Widget<Message, crate::Theme, crate::Renderer>
349 for Overlay<'a, Message>
350{
351 fn size(&self) -> Size<Length> {
352 Size::new(Length::Fixed(self.width), Length::Shrink)
353 }
354
355 fn layout(
356 &self,
357 _tree: &mut iced_core::widget::Tree,
358 renderer: &crate::Renderer,
359 limits: &iced::Limits,
360 ) -> layout::Node {
361 let limits = limits.width(self.width);
362
363 self.state
364 .with_data_mut(|tree| self.container.layout(tree, renderer, &limits))
365 }
366
367 fn mouse_interaction(
368 &self,
369 _tree: &Tree,
370 layout: Layout<'_>,
371 cursor: mouse::Cursor,
372 viewport: &Rectangle,
373 renderer: &crate::Renderer,
374 ) -> mouse::Interaction {
375 self._mouse_interaction(layout, cursor, viewport, renderer)
376 }
377
378 fn on_event(
379 &mut self,
380 _tree: &mut Tree,
381 event: Event,
382 layout: Layout<'_>,
383 cursor: mouse::Cursor,
384 renderer: &crate::Renderer,
385 clipboard: &mut dyn Clipboard,
386 shell: &mut Shell<'_, Message>,
387 _viewport: &Rectangle,
388 ) -> event::Status {
389 self._on_event(event, layout, cursor, renderer, clipboard, shell)
390 }
391
392 fn draw(
393 &self,
394 tree: &Tree,
395 renderer: &mut crate::Renderer,
396 theme: &crate::Theme,
397 style: &renderer::Style,
398 layout: Layout<'_>,
399 cursor: mouse::Cursor,
400 _viewport: &Rectangle,
401 ) {
402 self._draw(renderer, theme, style, layout, cursor);
403 }
404}
405
406impl<'a, Message: Clone + 'a> From<Overlay<'a, Message>> for crate::Element<'a, Message> {
407 fn from(widget: Overlay<'a, Message>) -> Self {
408 Element::new(widget)
409 }
410}
411
412struct List<'a, S: AsRef<str>, Message>
413where
414 [S]: std::borrow::ToOwned,
415{
416 options: Cow<'a, [S]>,
417 icons: Cow<'a, [icon::Handle]>,
418 hovered_option: Arc<Mutex<Option<usize>>>,
419 selected_option: Option<usize>,
420 on_selected: Box<dyn FnMut(usize) -> Message + 'a>,
421 close_on_selected: Option<Message>,
422 on_option_hovered: Option<&'a dyn Fn(usize) -> Message>,
423 padding: Padding,
424 text_size: Option<f32>,
425 text_line_height: text::LineHeight,
426}
427
428impl<S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer> for List<'_, S, Message>
429where
430 [S]: std::borrow::ToOwned,
431 Message: Clone,
432{
433 fn size(&self) -> Size<Length> {
434 Size::new(Length::Fill, Length::Shrink)
435 }
436
437 fn layout(
438 &self,
439 _tree: &mut Tree,
440 renderer: &crate::Renderer,
441 limits: &layout::Limits,
442 ) -> layout::Node {
443 use std::f32;
444
445 let limits = limits.width(Length::Fill).height(Length::Shrink);
446 let text_size = self
447 .text_size
448 .unwrap_or_else(|| text::Renderer::default_size(renderer).0);
449
450 let text_line_height = self.text_line_height.to_absolute(Pixels(text_size));
451
452 let size = {
453 let intrinsic = Size::new(
454 0.0,
455 (f32::from(text_line_height) + self.padding.vertical()) * self.options.len() as f32,
456 );
457
458 limits.resolve(Length::Fill, Length::Shrink, intrinsic)
459 };
460
461 layout::Node::new(size)
462 }
463
464 fn on_event(
465 &mut self,
466 _state: &mut Tree,
467 event: Event,
468 layout: Layout<'_>,
469 cursor: mouse::Cursor,
470 renderer: &crate::Renderer,
471 _clipboard: &mut dyn Clipboard,
472 shell: &mut Shell<'_, Message>,
473 _viewport: &Rectangle,
474 ) -> event::Status {
475 match event {
476 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
477 let hovered_guard = self.hovered_option.lock().unwrap();
478 if cursor.is_over(layout.bounds()) {
479 if let Some(index) = *hovered_guard {
480 shell.publish((self.on_selected)(index));
481 if let Some(close_on_selected) = self.close_on_selected.as_ref() {
482 shell.publish(close_on_selected.clone());
483 }
484 return event::Status::Captured;
485 }
486 }
487 }
488 Event::Mouse(mouse::Event::CursorMoved { .. }) => {
489 if let Some(cursor_position) = cursor.position_in(layout.bounds()) {
490 let text_size = self
491 .text_size
492 .unwrap_or_else(|| text::Renderer::default_size(renderer).0);
493
494 let option_height =
495 f32::from(self.text_line_height.to_absolute(Pixels(text_size)))
496 + self.padding.vertical();
497
498 let new_hovered_option = (cursor_position.y / option_height) as usize;
499 let mut hovered_guard = self.hovered_option.lock().unwrap();
500
501 if let Some(on_option_hovered) = self.on_option_hovered {
502 if *hovered_guard != Some(new_hovered_option) {
503 shell.publish(on_option_hovered(new_hovered_option));
504 }
505 }
506
507 *hovered_guard = Some(new_hovered_option);
508 }
509 }
510 Event::Touch(touch::Event::FingerPressed { .. }) => {
511 if let Some(cursor_position) = cursor.position_in(layout.bounds()) {
512 let text_size = self
513 .text_size
514 .unwrap_or_else(|| text::Renderer::default_size(renderer).0);
515
516 let option_height =
517 f32::from(self.text_line_height.to_absolute(Pixels(text_size)))
518 + self.padding.vertical();
519 let mut hovered_guard = self.hovered_option.lock().unwrap();
520
521 *hovered_guard = Some((cursor_position.y / option_height) as usize);
522
523 if let Some(index) = *hovered_guard {
524 shell.publish((self.on_selected)(index));
525 if let Some(close_on_selected) = self.close_on_selected.as_ref() {
526 shell.publish(close_on_selected.clone());
527 }
528 return event::Status::Captured;
529 }
530 }
531 }
532 _ => {}
533 }
534
535 event::Status::Ignored
536 }
537
538 fn mouse_interaction(
539 &self,
540 _state: &Tree,
541 layout: Layout<'_>,
542 cursor: mouse::Cursor,
543 _viewport: &Rectangle,
544 _renderer: &crate::Renderer,
545 ) -> mouse::Interaction {
546 let is_mouse_over = cursor.is_over(layout.bounds());
547
548 if is_mouse_over {
549 mouse::Interaction::Pointer
550 } else {
551 mouse::Interaction::default()
552 }
553 }
554
555 fn draw(
556 &self,
557 state: &Tree,
558 renderer: &mut crate::Renderer,
559 theme: &crate::Theme,
560 style: &renderer::Style,
561 layout: Layout<'_>,
562 cursor: mouse::Cursor,
563 viewport: &Rectangle,
564 ) {
565 let appearance = theme.appearance(&());
566 let bounds = layout.bounds();
567
568 let text_size = self
569 .text_size
570 .unwrap_or_else(|| text::Renderer::default_size(renderer).0);
571 let option_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size)))
572 + self.padding.vertical();
573
574 let offset = viewport.y - bounds.y;
575 let start = (offset / option_height) as usize;
576 let end = ((offset + viewport.height) / option_height).ceil() as usize;
577
578 let visible_options = &self.options[start..end.min(self.options.len())];
579
580 for (i, option) in visible_options.iter().enumerate() {
581 let i = start + i;
582
583 let bounds = Rectangle {
584 x: bounds.x,
585 y: option_height.mul_add(i as f32, bounds.y),
586 width: bounds.width,
587 height: option_height,
588 };
589
590 let hovered_guard = self.hovered_option.lock().unwrap();
591
592 let (color, font) = if self.selected_option == Some(i) {
593 let item_x = bounds.x + appearance.border_width;
594 let item_width = appearance.border_width.mul_add(-2.0, bounds.width);
595
596 renderer.fill_quad(
597 renderer::Quad {
598 bounds: Rectangle {
599 x: item_x,
600 width: item_width,
601 ..bounds
602 },
603 border: Border {
604 radius: appearance.border_radius,
605 ..Default::default()
606 },
607 shadow: Shadow::default(),
608 },
609 appearance.selected_background,
610 );
611
612 let svg_handle =
613 iced_core::Svg::new(crate::widget::common::object_select().clone())
614 .color(appearance.selected_text_color)
615 .border_radius(appearance.border_radius);
616
617 svg::Renderer::draw_svg(
618 renderer,
619 svg_handle,
620 Rectangle {
621 x: item_x + item_width - 16.0 - 8.0,
622 y: bounds.y + (bounds.height / 2.0 - 8.0),
623 width: 16.0,
624 height: 16.0,
625 },
626 );
627
628 (appearance.selected_text_color, crate::font::semibold())
629 } else if *hovered_guard == Some(i) {
630 let item_x = bounds.x + appearance.border_width;
631 let item_width = appearance.border_width.mul_add(-2.0, bounds.width);
632
633 renderer.fill_quad(
634 renderer::Quad {
635 bounds: Rectangle {
636 x: item_x,
637 width: item_width,
638 ..bounds
639 },
640 border: Border {
641 radius: appearance.border_radius,
642 ..Default::default()
643 },
644 shadow: Shadow::default(),
645 },
646 appearance.hovered_background,
647 );
648
649 (appearance.hovered_text_color, crate::font::default())
650 } else {
651 (appearance.text_color, crate::font::default())
652 };
653
654 let mut bounds = Rectangle {
655 x: bounds.x + self.padding.left,
656 y: bounds.center_y(),
657 width: f32::INFINITY,
658 ..bounds
659 };
660
661 if let Some(handle) = self.icons.get(i) {
662 let icon_bounds = Rectangle {
663 x: bounds.x,
664 y: bounds.y + 8.0 - (bounds.height / 2.0),
665 width: 20.0,
666 height: 20.0,
667 };
668
669 bounds.x += 24.0;
670 icon::draw(renderer, handle, icon_bounds);
671 }
672
673 text::Renderer::fill_text(
674 renderer,
675 Text {
676 content: option.as_ref().to_string(),
677 bounds: bounds.size(),
678 size: Pixels(text_size),
679 line_height: self.text_line_height,
680 font,
681 horizontal_alignment: alignment::Horizontal::Left,
682 vertical_alignment: alignment::Vertical::Center,
683 shaping: text::Shaping::Advanced,
684 wrapping: text::Wrapping::default(),
685 },
686 bounds.position(),
687 color,
688 *viewport,
689 );
690 }
691 }
692}
693
694impl<'a, S: AsRef<str>, Message: 'a> From<List<'a, S, Message>>
695 for Element<'a, Message, crate::Theme, crate::Renderer>
696where
697 [S]: std::borrow::ToOwned,
698 Message: Clone,
699{
700 fn from(list: List<'a, S, Message>) -> Self {
701 Element::new(list)
702 }
703}