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