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