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