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