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