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 if let Some(index) = *self.hovered_option {
412 if let Some(option) = self.options.get(index) {
413 shell.publish((self.on_selected)(option.clone()));
414 shell.capture_event();
415 }
416 }
417 }
418 }
419 Event::Mouse(mouse::Event::CursorMoved { .. }) => {
420 if let Some(cursor_position) =
421 cursor.position_in(layout.bounds())
422 {
423 let text_size = self
424 .text_size
425 .unwrap_or_else(|| renderer.default_size());
426
427 let option_height =
428 f32::from(self.text_line_height.to_absolute(text_size))
429 + self.padding.vertical();
430
431 let new_hovered_option =
432 (cursor_position.y / option_height) as usize;
433
434 if *self.hovered_option != Some(new_hovered_option) {
435 if let Some(option) =
436 self.options.get(new_hovered_option)
437 {
438 if let Some(on_option_hovered) =
439 self.on_option_hovered
440 {
441 shell
442 .publish(on_option_hovered(option.clone()));
443 }
444
445 shell.request_redraw();
446 }
447 }
448
449 *self.hovered_option = Some(new_hovered_option);
450 }
451 }
452 Event::Touch(touch::Event::FingerPressed { .. }) => {
453 if let Some(cursor_position) =
454 cursor.position_in(layout.bounds())
455 {
456 let text_size = self
457 .text_size
458 .unwrap_or_else(|| renderer.default_size());
459
460 let option_height =
461 f32::from(self.text_line_height.to_absolute(text_size))
462 + self.padding.vertical();
463
464 *self.hovered_option =
465 Some((cursor_position.y / option_height) as usize);
466
467 if let Some(index) = *self.hovered_option {
468 if let Some(option) = self.options.get(index) {
469 shell.publish((self.on_selected)(option.clone()));
470 shell.capture_event();
471 }
472 }
473 }
474 }
475 _ => {}
476 }
477
478 let state = tree.state.downcast_mut::<ListState>();
479
480 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
481 state.is_hovered = Some(cursor.is_over(layout.bounds()));
482 } else if state.is_hovered.is_some_and(|is_hovered| {
483 is_hovered != cursor.is_over(layout.bounds())
484 }) {
485 shell.request_redraw();
486 }
487 }
488
489 fn mouse_interaction(
490 &self,
491 _state: &Tree,
492 layout: Layout<'_>,
493 cursor: mouse::Cursor,
494 _viewport: &Rectangle,
495 _renderer: &Renderer,
496 ) -> mouse::Interaction {
497 let is_mouse_over = cursor.is_over(layout.bounds());
498
499 if is_mouse_over {
500 mouse::Interaction::Pointer
501 } else {
502 mouse::Interaction::default()
503 }
504 }
505
506 fn draw(
507 &self,
508 _state: &Tree,
509 renderer: &mut Renderer,
510 theme: &Theme,
511 _style: &renderer::Style,
512 layout: Layout<'_>,
513 _cursor: mouse::Cursor,
514 viewport: &Rectangle,
515 ) {
516 let style = Catalog::style(theme, self.class);
517 let bounds = layout.bounds();
518
519 let text_size =
520 self.text_size.unwrap_or_else(|| renderer.default_size());
521 let option_height =
522 f32::from(self.text_line_height.to_absolute(text_size))
523 + self.padding.vertical();
524
525 let offset = viewport.y - bounds.y;
526 let start = (offset / option_height) as usize;
527 let end = ((offset + viewport.height) / option_height).ceil() as usize;
528
529 let visible_options = &self.options[start..end.min(self.options.len())];
530
531 for (i, option) in visible_options.iter().enumerate() {
532 let i = start + i;
533 let is_selected = *self.hovered_option == Some(i);
534
535 let bounds = Rectangle {
536 x: bounds.x,
537 y: bounds.y + (option_height * i as f32),
538 width: bounds.width,
539 height: option_height,
540 };
541
542 if is_selected {
543 renderer.fill_quad(
544 renderer::Quad {
545 bounds: Rectangle {
546 x: bounds.x + style.border.width,
547 width: bounds.width - style.border.width * 2.0,
548 ..bounds
549 },
550 border: border::rounded(style.border.radius),
551 ..renderer::Quad::default()
552 },
553 style.selected_background,
554 );
555 }
556
557 renderer.fill_text(
558 Text {
559 content: option.to_string(),
560 bounds: Size::new(f32::INFINITY, bounds.height),
561 size: text_size,
562 line_height: self.text_line_height,
563 font: self.font.unwrap_or_else(|| renderer.default_font()),
564 align_x: text::Alignment::Default,
565 align_y: alignment::Vertical::Center,
566 shaping: self.text_shaping,
567 wrapping: text::Wrapping::default(),
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>
582 From<List<'a, 'b, T, Message, Theme, Renderer>>
583 for Element<'a, Message, Theme, Renderer>
584where
585 T: ToString + Clone,
586 Message: 'a,
587 Theme: 'a + Catalog,
588 Renderer: 'a + text::Renderer,
589 'b: 'a,
590{
591 fn from(list: List<'a, 'b, T, Message, Theme, Renderer>) -> Self {
592 Element::new(list)
593 }
594}
595
596#[derive(Debug, Clone, Copy, PartialEq)]
598pub struct Style {
599 pub background: Background,
601 pub border: Border,
603 pub text_color: Color,
605 pub selected_text_color: Color,
607 pub selected_background: Background,
609}
610
611pub trait Catalog: scrollable::Catalog {
613 type Class<'a>;
615
616 fn default<'a>() -> <Self as Catalog>::Class<'a>;
618
619 fn default_scrollable<'a>() -> <Self as scrollable::Catalog>::Class<'a> {
621 <Self as scrollable::Catalog>::default()
622 }
623
624 fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
626}
627
628pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
630
631impl Catalog for Theme {
632 type Class<'a> = StyleFn<'a, Self>;
633
634 fn default<'a>() -> StyleFn<'a, Self> {
635 Box::new(default)
636 }
637
638 fn style(&self, class: &StyleFn<'_, Self>) -> Style {
639 class(self)
640 }
641}
642
643pub fn default(theme: &Theme) -> Style {
645 let palette = theme.extended_palette();
646
647 Style {
648 background: palette.background.weak.color.into(),
649 border: Border {
650 width: 1.0,
651 radius: 0.0.into(),
652 color: palette.background.strong.color,
653 },
654 text_color: palette.background.weak.text,
655 selected_text_color: palette.primary.strong.text,
656 selected_background: palette.primary.strong.color.into(),
657 }
658}