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