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 target_height: f32,
131 ) -> overlay::Element<'a, Message, Theme, Renderer> {
132 overlay::Element::new(Box::new(Overlay::new(
133 position,
134 self,
135 target_height,
136 )))
137 }
138}
139
140#[derive(Debug)]
142pub struct State {
143 tree: Tree,
144}
145
146impl State {
147 pub fn new() -> Self {
149 Self {
150 tree: Tree::empty(),
151 }
152 }
153}
154
155impl Default for State {
156 fn default() -> Self {
157 Self::new()
158 }
159}
160
161struct Overlay<'a, 'b, Message, Theme, Renderer>
162where
163 Theme: Catalog,
164 Renderer: crate::core::Renderer,
165{
166 position: Point,
167 state: &'a mut Tree,
168 list: Scrollable<'a, Message, Theme, Renderer>,
169 width: f32,
170 target_height: f32,
171 class: &'a <Theme as Catalog>::Class<'b>,
172}
173
174impl<'a, 'b, Message, Theme, Renderer> Overlay<'a, 'b, Message, Theme, Renderer>
175where
176 Message: 'a,
177 Theme: Catalog + scrollable::Catalog + 'a,
178 Renderer: text::Renderer + 'a,
179 'b: 'a,
180{
181 pub fn new<T>(
182 position: Point,
183 menu: Menu<'a, 'b, T, Message, Theme, Renderer>,
184 target_height: f32,
185 ) -> Self
186 where
187 T: Clone + ToString,
188 {
189 let Menu {
190 state,
191 options,
192 hovered_option,
193 on_selected,
194 on_option_hovered,
195 width,
196 padding,
197 font,
198 text_size,
199 text_line_height,
200 text_shaping,
201 class,
202 } = menu;
203
204 let list = Scrollable::new(List {
205 options,
206 hovered_option,
207 on_selected,
208 on_option_hovered,
209 font,
210 text_size,
211 text_line_height,
212 text_shaping,
213 padding,
214 class,
215 });
216
217 state.tree.diff(&list as &dyn Widget<_, _, _>);
218
219 Self {
220 position,
221 state: &mut state.tree,
222 list,
223 width,
224 target_height,
225 class,
226 }
227 }
228}
229
230impl<Message, Theme, Renderer> crate::core::Overlay<Message, Theme, Renderer>
231 for Overlay<'_, '_, Message, Theme, Renderer>
232where
233 Theme: Catalog,
234 Renderer: text::Renderer,
235{
236 fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
237 let space_below =
238 bounds.height - (self.position.y + self.target_height);
239 let space_above = self.position.y;
240
241 let limits = layout::Limits::new(
242 Size::ZERO,
243 Size::new(
244 bounds.width - self.position.x,
245 if space_below > space_above {
246 space_below
247 } else {
248 space_above
249 },
250 ),
251 )
252 .width(self.width);
253
254 let node = self.list.layout(self.state, renderer, &limits);
255 let size = node.size();
256
257 node.move_to(if space_below > space_above {
258 self.position + Vector::new(0.0, self.target_height)
259 } else {
260 self.position - Vector::new(0.0, size.height)
261 })
262 }
263
264 fn update(
265 &mut self,
266 event: &Event,
267 layout: Layout<'_>,
268 cursor: mouse::Cursor,
269 renderer: &Renderer,
270 clipboard: &mut dyn Clipboard,
271 shell: &mut Shell<'_, Message>,
272 ) {
273 let bounds = layout.bounds();
274
275 self.list.update(
276 self.state, event, layout, cursor, renderer, clipboard, shell,
277 &bounds,
278 );
279 }
280
281 fn mouse_interaction(
282 &self,
283 layout: Layout<'_>,
284 cursor: mouse::Cursor,
285 viewport: &Rectangle,
286 renderer: &Renderer,
287 ) -> mouse::Interaction {
288 self.list
289 .mouse_interaction(self.state, layout, cursor, viewport, renderer)
290 }
291
292 fn draw(
293 &self,
294 renderer: &mut Renderer,
295 theme: &Theme,
296 defaults: &renderer::Style,
297 layout: Layout<'_>,
298 cursor: mouse::Cursor,
299 ) {
300 let bounds = layout.bounds();
301
302 let style = Catalog::style(theme, self.class);
303
304 renderer.fill_quad(
305 renderer::Quad {
306 bounds,
307 border: style.border,
308 ..renderer::Quad::default()
309 },
310 style.background,
311 );
312
313 self.list.draw(
314 self.state, renderer, theme, defaults, layout, cursor, &bounds,
315 );
316 }
317}
318
319struct List<'a, 'b, T, Message, Theme, Renderer>
320where
321 Theme: Catalog,
322 Renderer: text::Renderer,
323{
324 options: &'a [T],
325 hovered_option: &'a mut Option<usize>,
326 on_selected: Box<dyn FnMut(T) -> Message + 'a>,
327 on_option_hovered: Option<&'a dyn Fn(T) -> Message>,
328 padding: Padding,
329 text_size: Option<Pixels>,
330 text_line_height: text::LineHeight,
331 text_shaping: text::Shaping,
332 font: Option<Renderer::Font>,
333 class: &'a <Theme as Catalog>::Class<'b>,
334}
335
336struct ListState {
337 is_hovered: Option<bool>,
338}
339
340impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
341 for List<'_, '_, T, Message, Theme, Renderer>
342where
343 T: Clone + ToString,
344 Theme: Catalog,
345 Renderer: text::Renderer,
346{
347 fn tag(&self) -> tree::Tag {
348 tree::Tag::of::<Option<bool>>()
349 }
350
351 fn state(&self) -> tree::State {
352 tree::State::new(ListState { is_hovered: None })
353 }
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 update(
389 &mut self,
390 tree: &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 ) {
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 shell.capture_event();
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 *self.hovered_option != Some(new_hovered_option) {
426 if let Some(option) =
427 self.options.get(new_hovered_option)
428 {
429 if let Some(on_option_hovered) =
430 self.on_option_hovered
431 {
432 shell
433 .publish(on_option_hovered(option.clone()));
434 }
435
436 shell.request_redraw();
437 }
438 }
439
440 *self.hovered_option = Some(new_hovered_option);
441 }
442 }
443 Event::Touch(touch::Event::FingerPressed { .. }) => {
444 if let Some(cursor_position) =
445 cursor.position_in(layout.bounds())
446 {
447 let text_size = self
448 .text_size
449 .unwrap_or_else(|| renderer.default_size());
450
451 let option_height =
452 f32::from(self.text_line_height.to_absolute(text_size))
453 + self.padding.vertical();
454
455 *self.hovered_option =
456 Some((cursor_position.y / option_height) as usize);
457
458 if let Some(index) = *self.hovered_option {
459 if let Some(option) = self.options.get(index) {
460 shell.publish((self.on_selected)(option.clone()));
461 shell.capture_event();
462 }
463 }
464 }
465 }
466 _ => {}
467 }
468
469 let state = tree.state.downcast_mut::<ListState>();
470
471 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
472 state.is_hovered = Some(cursor.is_over(layout.bounds()));
473 } else if state.is_hovered.is_some_and(|is_hovered| {
474 is_hovered != cursor.is_over(layout.bounds())
475 }) {
476 shell.request_redraw();
477 }
478 }
479
480 fn mouse_interaction(
481 &self,
482 _state: &Tree,
483 layout: Layout<'_>,
484 cursor: mouse::Cursor,
485 _viewport: &Rectangle,
486 _renderer: &Renderer,
487 ) -> mouse::Interaction {
488 let is_mouse_over = cursor.is_over(layout.bounds());
489
490 if is_mouse_over {
491 mouse::Interaction::Pointer
492 } else {
493 mouse::Interaction::default()
494 }
495 }
496
497 fn draw(
498 &self,
499 _state: &Tree,
500 renderer: &mut Renderer,
501 theme: &Theme,
502 _style: &renderer::Style,
503 layout: Layout<'_>,
504 _cursor: mouse::Cursor,
505 viewport: &Rectangle,
506 ) {
507 let style = Catalog::style(theme, self.class);
508 let bounds = layout.bounds();
509
510 let text_size =
511 self.text_size.unwrap_or_else(|| renderer.default_size());
512 let option_height =
513 f32::from(self.text_line_height.to_absolute(text_size))
514 + self.padding.vertical();
515
516 let offset = viewport.y - bounds.y;
517 let start = (offset / option_height) as usize;
518 let end = ((offset + viewport.height) / option_height).ceil() as usize;
519
520 let visible_options = &self.options[start..end.min(self.options.len())];
521
522 for (i, option) in visible_options.iter().enumerate() {
523 let i = start + i;
524 let is_selected = *self.hovered_option == Some(i);
525
526 let bounds = Rectangle {
527 x: bounds.x,
528 y: bounds.y + (option_height * i as f32),
529 width: bounds.width,
530 height: option_height,
531 };
532
533 if is_selected {
534 renderer.fill_quad(
535 renderer::Quad {
536 bounds: Rectangle {
537 x: bounds.x + style.border.width,
538 width: bounds.width - style.border.width * 2.0,
539 ..bounds
540 },
541 border: border::rounded(style.border.radius),
542 ..renderer::Quad::default()
543 },
544 style.selected_background,
545 );
546 }
547
548 renderer.fill_text(
549 Text {
550 content: option.to_string(),
551 bounds: Size::new(f32::INFINITY, bounds.height),
552 size: text_size,
553 line_height: self.text_line_height,
554 font: self.font.unwrap_or_else(|| renderer.default_font()),
555 align_x: text::Alignment::Default,
556 align_y: alignment::Vertical::Center,
557 shaping: self.text_shaping,
558 wrapping: text::Wrapping::default(),
559 },
560 Point::new(bounds.x + self.padding.left, bounds.center_y()),
561 if is_selected {
562 style.selected_text_color
563 } else {
564 style.text_color
565 },
566 *viewport,
567 );
568 }
569 }
570}
571
572impl<'a, 'b, T, Message, Theme, Renderer>
573 From<List<'a, 'b, T, Message, Theme, Renderer>>
574 for Element<'a, Message, Theme, Renderer>
575where
576 T: ToString + Clone,
577 Message: 'a,
578 Theme: 'a + Catalog,
579 Renderer: 'a + text::Renderer,
580 'b: 'a,
581{
582 fn from(list: List<'a, 'b, T, Message, Theme, Renderer>) -> Self {
583 Element::new(list)
584 }
585}
586
587#[derive(Debug, Clone, Copy, PartialEq)]
589pub struct Style {
590 pub background: Background,
592 pub border: Border,
594 pub text_color: Color,
596 pub selected_text_color: Color,
598 pub selected_background: Background,
600}
601
602pub trait Catalog: scrollable::Catalog {
604 type Class<'a>;
606
607 fn default<'a>() -> <Self as Catalog>::Class<'a>;
609
610 fn default_scrollable<'a>() -> <Self as scrollable::Catalog>::Class<'a> {
612 <Self as scrollable::Catalog>::default()
613 }
614
615 fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
617}
618
619pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
621
622impl Catalog for Theme {
623 type Class<'a> = StyleFn<'a, Self>;
624
625 fn default<'a>() -> StyleFn<'a, Self> {
626 Box::new(default)
627 }
628
629 fn style(&self, class: &StyleFn<'_, Self>) -> Style {
630 class(self)
631 }
632}
633
634pub fn default(theme: &Theme) -> Style {
636 let palette = theme.extended_palette();
637
638 Style {
639 background: palette.background.weak.color.into(),
640 border: Border {
641 width: 1.0,
642 radius: 0.0.into(),
643 color: palette.background.strong.color,
644 },
645 text_color: palette.background.weak.text,
646 selected_text_color: palette.primary.strong.text,
647 selected_background: palette.primary.strong.color.into(),
648 }
649}