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