1use std::borrow::Borrow;
9use std::f32::consts::PI;
10use std::fmt;
11
12use iced_widget::canvas::{Frame, Path};
13use iced_widget::core::text::paragraph;
14use iced_widget::core::text::{self, Text};
15use iced_widget::core::time::Instant;
16use iced_widget::core::widget::tree::{self, Tree};
17use iced_widget::core::{
18 Clipboard, Color, Element, Event, Layout, Length, Padding, Pixels, Point, Rectangle, Shell,
19 Size, Vector, Widget, alignment, keyboard, layout, mouse, overlay, renderer, touch, window,
20};
21use iced_widget::graphics::geometry;
22use iced_widget::overlay::menu;
23use iced_widget::pick_list::{self as iced_select, Handle, Icon, Status};
24
25use super::support::{AnimatedScalar, duration_ms};
26use super::{
27 absolute_line_height, draw_text_field_outline, menu_overlay, text_field_floating_label_notch,
28};
29use crate::style::{menu as menu_style, select as select_style};
30use crate::{Theme, tokens};
31
32const MAX_VISIBLE_OPTIONS: usize = 5;
33const DIRECTION_EPSILON: f32 = 0.5;
34const MENU_HANDLE_ROTATION_DURATION_MS: u16 = tokens::motion::DURATION_SHORT3_MS;
35const MENU_HANDLE_VIEWPORT_SIZE: f32 = 24.0;
36const MENU_HANDLE_ARROW_LEFT_X: f32 = 7.0;
37const MENU_HANDLE_ARROW_CENTER_X: f32 = 12.0;
38const MENU_HANDLE_ARROW_RIGHT_X: f32 = 17.0;
39const MENU_HANDLE_ARROW_TOP_Y: f32 = 10.0;
40const MENU_HANDLE_ARROW_BOTTOM_Y: f32 = 15.0;
41
42#[derive(Debug, Clone, Copy, PartialEq)]
43pub(crate) struct MenuAnchor {
44 pub(crate) position: Point,
45 pub(crate) target_height: f32,
46}
47
48pub fn outlined<'a, T, L, V, Message, Renderer>(
50 options: L,
51 selected: Option<V>,
52 on_select: impl Fn(T) -> Message + 'a,
53) -> Select<'a, T, L, V, Message, Renderer>
54where
55 T: ToString + PartialEq + Clone + 'a,
56 L: Borrow<[T]> + 'a,
57 V: Borrow<T> + 'a,
58 Message: Clone + 'a,
59 Renderer: text::Renderer + 'a,
60{
61 Select::new(options, selected, on_select)
62 .padding(Padding {
63 top: tokens::component::text_field::TOP_SPACE,
64 right: tokens::component::text_field::TRAILING_SPACE,
65 bottom: tokens::component::text_field::BOTTOM_SPACE,
66 left: tokens::component::text_field::LEADING_SPACE,
67 })
68 .option_padding(menu_option_padding())
69 .text_size(tokens::component::text_field::INPUT_TEXT_SIZE)
70 .text_line_height(absolute_line_height(
71 tokens::component::text_field::INPUT_TEXT_LINE_HEIGHT,
72 ))
73 .width(Length::Fill)
74 .style(select_style::default)
75 .menu_style(menu_style::outlined_select)
76}
77
78pub struct Select<'a, T, L, V, Message, Renderer>
80where
81 T: ToString + PartialEq + Clone,
82 L: Borrow<[T]> + 'a,
83 V: Borrow<T> + 'a,
84 Renderer: text::Renderer,
85{
86 on_select: Box<dyn Fn(T) -> Message + 'a>,
87 on_open: Option<Message>,
88 on_close: Option<Message>,
89 options: L,
90 label: Option<String>,
91 placeholder: Option<String>,
92 selected: Option<V>,
93 width: Length,
94 field_padding: Padding,
95 option_padding: Padding,
96 text_size: Option<Pixels>,
97 text_line_height: text::LineHeight,
98 text_shaping: text::Shaping,
99 font: Option<Renderer::Font>,
100 handle: Handle<Renderer::Font>,
101 class: <Theme as iced_select::Catalog>::Class<'a>,
102 menu_class: <Theme as menu::Catalog>::Class<'a>,
103 menu_height: Length,
104}
105
106impl<T, L, V, Message, Renderer> fmt::Debug for Select<'_, T, L, V, Message, Renderer>
107where
108 T: ToString + PartialEq + Clone,
109 L: Borrow<[T]>,
110 V: Borrow<T>,
111 Renderer: text::Renderer,
112{
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 f.debug_struct("Select").finish_non_exhaustive()
115 }
116}
117
118impl<'a, T, L, V, Message, Renderer> Select<'a, T, L, V, Message, Renderer>
119where
120 T: ToString + PartialEq + Clone,
121 L: Borrow<[T]> + 'a,
122 V: Borrow<T> + 'a,
123 Message: Clone,
124 Renderer: text::Renderer,
125{
126 pub fn new(options: L, selected: Option<V>, on_select: impl Fn(T) -> Message + 'a) -> Self {
129 let option_count = options.borrow().len();
130
131 Self {
132 on_select: Box::new(on_select),
133 on_open: None,
134 on_close: None,
135 options,
136 label: None,
137 placeholder: None,
138 selected,
139 width: Length::Shrink,
140 field_padding: iced_widget::button::DEFAULT_PADDING,
141 option_padding: menu_option_padding(),
142 text_size: None,
143 text_line_height: text::LineHeight::default(),
144 text_shaping: text::Shaping::default(),
145 font: None,
146 handle: Handle::default(),
147 class: <Theme as iced_select::Catalog>::default(),
148 menu_class: <Theme as iced_select::Catalog>::default_menu(),
149 menu_height: material_menu_height(option_count),
150 }
151 }
152
153 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
155 self.placeholder = Some(placeholder.into());
156 self
157 }
158
159 pub fn label(mut self, label: impl Into<String>) -> Self {
161 self.label = Some(label.into());
162 self
163 }
164
165 pub fn width(mut self, width: impl Into<Length>) -> Self {
167 self.width = width.into();
168 self
169 }
170
171 pub fn menu_height(mut self, menu_height: impl Into<Length>) -> Self {
173 self.menu_height = menu_height.into();
174 self
175 }
176
177 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
179 self.field_padding = padding.into();
180 self
181 }
182
183 pub fn option_padding(mut self, padding: impl Into<Padding>) -> Self {
185 self.option_padding = padding.into();
186 self
187 }
188
189 pub fn text_size(mut self, size: impl Into<Pixels>) -> Self {
191 self.text_size = Some(size.into());
192 self
193 }
194
195 pub fn text_line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
197 self.text_line_height = line_height.into();
198 self
199 }
200
201 pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
203 self.text_shaping = shaping;
204 self
205 }
206
207 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
209 self.font = Some(font.into());
210 self
211 }
212
213 pub fn handle(mut self, handle: Handle<Renderer::Font>) -> Self {
215 self.handle = handle;
216 self
217 }
218
219 pub fn on_open(mut self, on_open: Message) -> Self {
221 self.on_open = Some(on_open);
222 self
223 }
224
225 pub fn on_close(mut self, on_close: Message) -> Self {
227 self.on_close = Some(on_close);
228 self
229 }
230
231 pub fn style(mut self, style: impl Fn(&Theme, Status) -> iced_select::Style + 'a) -> Self
233 where
234 <Theme as iced_select::Catalog>::Class<'a>: From<iced_select::StyleFn<'a, Theme>>,
235 {
236 self.class = Box::new(style) as iced_select::StyleFn<'a, Theme>;
237 self
238 }
239
240 pub fn menu_style(mut self, style: impl Fn(&Theme) -> menu::Style + 'a) -> Self
242 where
243 <Theme as menu::Catalog>::Class<'a>: From<menu::StyleFn<'a, Theme>>,
244 {
245 self.menu_class = Box::new(style) as menu::StyleFn<'a, Theme>;
246 self
247 }
248
249 fn intrinsic_menu_height(&self, renderer: &Renderer) -> f32 {
250 let text_size = self.text_size.unwrap_or_else(|| renderer.default_size());
251 let option_height =
252 f32::from(self.text_line_height.to_absolute(text_size)) + self.option_padding.y();
253
254 option_height * self.options.borrow().len() as f32
255 }
256}
257
258impl<'a, T, L, V, Message, Renderer> Widget<Message, Theme, Renderer>
259 for Select<'a, T, L, V, Message, Renderer>
260where
261 T: Clone + ToString + PartialEq + 'a,
262 L: Borrow<[T]>,
263 V: Borrow<T>,
264 Message: Clone + 'a,
265 Renderer: text::Renderer + geometry::Renderer + 'a,
266{
267 fn tag(&self) -> tree::Tag {
268 tree::Tag::of::<State<Renderer::Paragraph>>()
269 }
270
271 fn state(&self) -> tree::State {
272 tree::State::new(State::<Renderer::Paragraph>::new())
273 }
274
275 fn size(&self) -> Size<Length> {
276 Size {
277 width: self.width,
278 height: Length::Shrink,
279 }
280 }
281
282 fn layout(
283 &mut self,
284 tree: &mut Tree,
285 renderer: &Renderer,
286 limits: &layout::Limits,
287 ) -> layout::Node {
288 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
289
290 let font = self.font.unwrap_or_else(|| renderer.default_font());
291 let text_size = self.text_size.unwrap_or_else(|| renderer.default_size());
292 let options = self.options.borrow();
293
294 state.options.resize_with(options.len(), Default::default);
295
296 let option_text = Text {
297 content: "",
298 bounds: Size::new(
299 f32::INFINITY,
300 self.text_line_height.to_absolute(text_size).into(),
301 ),
302 size: text_size,
303 line_height: self.text_line_height,
304 font,
305 align_x: text::Alignment::Default,
306 align_y: alignment::Vertical::Center,
307 shaping: self.text_shaping,
308 wrapping: text::Wrapping::default(),
309 };
310
311 for (option, paragraph) in options.iter().zip(state.options.iter_mut()) {
312 let label = option.to_string();
313
314 let _ = paragraph.update(Text {
315 content: &label,
316 ..option_text
317 });
318 }
319
320 if let Some(placeholder) = &self.placeholder {
321 let _ = state.placeholder.update(Text {
322 content: placeholder,
323 ..option_text
324 });
325 }
326
327 if let Some(label) = &self.label {
328 let _ = state.label.update(Text {
329 content: label,
330 size: Pixels(tokens::component::text_field::LABEL_TEXT_POPULATED_SIZE),
331 line_height: text::LineHeight::Absolute(Pixels(
332 tokens::component::text_field::LABEL_TEXT_POPULATED_LINE_HEIGHT,
333 )),
334 ..option_text
335 });
336 }
337
338 let max_width = match self.width {
339 Length::Shrink => {
340 let labels_width = state.options.iter().fold(0.0, |width, paragraph| {
341 f32::max(width, paragraph.min_width())
342 });
343
344 labels_width
345 .max(
346 self.placeholder
347 .as_ref()
348 .map(|_| state.placeholder.min_width())
349 .unwrap_or(0.0),
350 )
351 .max(
352 self.label
353 .as_ref()
354 .map(|_| state.label.min_width())
355 .unwrap_or(0.0),
356 )
357 }
358 _ => 0.0,
359 };
360
361 let size = {
362 let intrinsic = Size::new(
363 max_width + text_size.0 + self.field_padding.left,
364 f32::from(self.text_line_height.to_absolute(text_size)),
365 );
366
367 limits
368 .width(self.width)
369 .shrink(self.field_padding)
370 .resolve(self.width, Length::Shrink, intrinsic)
371 .expand(self.field_padding)
372 };
373
374 layout::Node::new(size)
375 }
376
377 fn update(
378 &mut self,
379 tree: &mut Tree,
380 event: &Event,
381 layout: Layout<'_>,
382 cursor: mouse::Cursor,
383 _renderer: &Renderer,
384 _clipboard: &mut dyn Clipboard,
385 shell: &mut Shell<'_, Message>,
386 _viewport: &Rectangle,
387 ) {
388 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
389
390 match event {
391 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
392 | Event::Touch(touch::Event::FingerPressed { .. }) => {
393 if state.is_open {
394 let now = Instant::now();
395
396 state.set_open(false, now);
397
398 if let Some(on_close) = &self.on_close {
399 shell.publish(on_close.clone());
400 }
401
402 shell.capture_event();
403 } else if cursor.is_over(layout.bounds()) {
404 let selected = self.selected.as_ref().map(Borrow::borrow);
405 let now = Instant::now();
406
407 state.set_open(true, now);
408 state.hovered_option = self
409 .options
410 .borrow()
411 .iter()
412 .position(|option| Some(option) == selected);
413
414 if let Some(on_open) = &self.on_open {
415 shell.publish(on_open.clone());
416 }
417
418 shell.capture_event();
419 }
420 }
421 Event::Mouse(mouse::Event::WheelScrolled {
422 delta: mouse::ScrollDelta::Lines { y, .. },
423 }) if state.keyboard_modifiers.command()
424 && cursor.is_over(layout.bounds())
425 && !state.is_open =>
426 {
427 let options = self.options.borrow();
428 let selected = self.selected.as_ref().map(Borrow::borrow);
429
430 let next_option = if *y < 0.0 {
431 if let Some(selected) = selected {
432 find_next(selected, options.iter())
433 } else {
434 options.first()
435 }
436 } else if *y > 0.0 {
437 if let Some(selected) = selected {
438 find_next(selected, options.iter().rev())
439 } else {
440 options.last()
441 }
442 } else {
443 None
444 };
445
446 if let Some(next_option) = next_option {
447 shell.publish((self.on_select)(next_option.clone()));
448 }
449
450 shell.capture_event();
451 }
452 Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
453 state.keyboard_modifiers = *modifiers;
454 }
455 _ => {}
456 };
457
458 let now = match event {
459 Event::Window(window::Event::RedrawRequested(now)) => Some(*now),
460 _ => None,
461 };
462
463 if let Some(now) = now
464 && state.advance(now)
465 {
466 shell.request_redraw();
467 }
468
469 let status = select_status(state.is_open, cursor.is_over(layout.bounds()));
470
471 if state.last_status != Some(status) {
472 state.last_status = Some(status);
473 shell.request_redraw();
474 } else if state.is_animating() {
475 shell.request_redraw();
476 }
477 }
478
479 fn mouse_interaction(
480 &self,
481 _tree: &Tree,
482 layout: Layout<'_>,
483 cursor: mouse::Cursor,
484 _viewport: &Rectangle,
485 _renderer: &Renderer,
486 ) -> mouse::Interaction {
487 if cursor.is_over(layout.bounds()) {
488 mouse::Interaction::Pointer
489 } else {
490 mouse::Interaction::default()
491 }
492 }
493
494 fn draw(
495 &self,
496 tree: &Tree,
497 renderer: &mut Renderer,
498 theme: &Theme,
499 _style: &renderer::Style,
500 layout: Layout<'_>,
501 cursor: mouse::Cursor,
502 viewport: &Rectangle,
503 ) {
504 let font = self.font.unwrap_or_else(|| renderer.default_font());
505 let selected = self.selected.as_ref().map(Borrow::borrow);
506 let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
507
508 let bounds = layout.bounds();
509 let status = state
510 .last_status
511 .unwrap_or_else(|| select_status(state.is_open, cursor.is_over(bounds)));
512
513 let style = <Theme as iced_select::Catalog>::style(theme, &self.class, status);
514
515 let label_width = self
516 .label
517 .as_ref()
518 .map(|_| state.label.min_width())
519 .unwrap_or(0.0);
520 let label_x = bounds.x + tokens::component::text_field::LEADING_SPACE;
521 let label_notch = self.label.as_ref().and_then(|_| {
522 text_field_floating_label_notch(bounds, label_x, label_width, label_width, 1.0)
523 });
524
525 draw_text_field_outline(
526 renderer,
527 bounds,
528 style.background,
529 style.border,
530 label_notch,
531 );
532
533 let text_handle = match &self.handle {
534 Handle::Arrow { size } => {
535 let size = size.unwrap_or(Pixels(tokens::component::select::TRAILING_ICON_SIZE));
536 let right = bounds.x + bounds.width - self.field_padding.right;
537 let center = Point::new(right - size.0 / 2.0, bounds.center_y());
538
539 draw_default_handle(
540 renderer,
541 center,
542 size.0,
543 state.handle_rotation.value,
544 style.handle_color,
545 );
546
547 None
548 }
549 Handle::Static(Icon {
550 font,
551 code_point,
552 size,
553 line_height,
554 shaping,
555 }) => Some((*font, *code_point, *size, *line_height, *shaping)),
556 Handle::Dynamic { open, closed } => {
557 if state.is_open {
558 Some((
559 open.font,
560 open.code_point,
561 open.size,
562 open.line_height,
563 open.shaping,
564 ))
565 } else {
566 Some((
567 closed.font,
568 closed.code_point,
569 closed.size,
570 closed.line_height,
571 closed.shaping,
572 ))
573 }
574 }
575 Handle::None => None,
576 };
577
578 if let Some((font, code_point, size, line_height, shaping)) = text_handle {
579 let size = size.unwrap_or_else(|| renderer.default_size());
580
581 renderer.fill_text(
582 Text {
583 content: code_point.to_string(),
584 size,
585 line_height,
586 font,
587 bounds: Size::new(bounds.width, f32::from(line_height.to_absolute(size))),
588 align_x: text::Alignment::Right,
589 align_y: alignment::Vertical::Center,
590 shaping,
591 wrapping: text::Wrapping::default(),
592 },
593 Point::new(
594 bounds.x + bounds.width - self.field_padding.right,
595 bounds.center_y(),
596 ),
597 style.handle_color,
598 *viewport,
599 );
600 }
601
602 let label = selected.map(ToString::to_string);
603
604 if let Some(label) = label.or_else(|| self.placeholder.clone()) {
605 let text_size = self.text_size.unwrap_or_else(|| renderer.default_size());
606
607 renderer.fill_text(
608 Text {
609 content: label,
610 size: text_size,
611 line_height: self.text_line_height,
612 font,
613 bounds: Size::new(
614 bounds.width - self.field_padding.x(),
615 f32::from(self.text_line_height.to_absolute(text_size)),
616 ),
617 align_x: text::Alignment::Default,
618 align_y: alignment::Vertical::Center,
619 shaping: self.text_shaping,
620 wrapping: text::Wrapping::default(),
621 },
622 Point::new(bounds.x + self.field_padding.left, bounds.center_y()),
623 if selected.is_some() {
624 style.text_color
625 } else {
626 style.placeholder_color
627 },
628 *viewport,
629 );
630 }
631
632 if let Some(label) = &self.label {
633 let label_size = Pixels(tokens::component::text_field::LABEL_TEXT_POPULATED_SIZE);
634 let label_line_height = text::LineHeight::Absolute(Pixels(
635 tokens::component::text_field::LABEL_TEXT_POPULATED_LINE_HEIGHT,
636 ));
637 let label_height = f32::from(label_line_height.to_absolute(label_size));
638 let label_y = bounds.y;
639
640 renderer.fill_text(
641 Text {
642 content: label.clone(),
643 size: label_size,
644 line_height: label_line_height,
645 font,
646 bounds: Size::new(label_width, label_height),
647 align_x: text::Alignment::Default,
648 align_y: alignment::Vertical::Center,
649 shaping: self.text_shaping,
650 wrapping: text::Wrapping::None,
651 },
652 Point::new(label_x, label_y),
653 select_label_color(theme, status),
654 *viewport,
655 );
656 }
657 }
658
659 fn overlay<'b>(
660 &'b mut self,
661 tree: &'b mut Tree,
662 layout: Layout<'_>,
663 renderer: &Renderer,
664 viewport: &Rectangle,
665 translation: Vector,
666 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
667 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
668 let font = self.font.unwrap_or_else(|| renderer.default_font());
669
670 if state.menu.is_visible() {
671 let bounds = layout.bounds();
672 let on_select = &self.on_select;
673 let menu_state = &mut state.menu;
674 let hovered_option = &mut state.hovered_option;
675 let open_state = &mut state.is_open;
676 let handle_rotation = &mut state.handle_rotation;
677 let last_status = &mut state.last_status;
678
679 let mut menu = menu_overlay::Menu::new(
680 menu_state,
681 self.options.borrow(),
682 hovered_option,
683 on_select,
684 None,
685 &self.menu_class,
686 )
687 .on_dismiss(move |now| {
688 set_menu_open(open_state, handle_rotation, last_status, false, now);
689 })
690 .width(bounds.width)
691 .padding(self.option_padding)
692 .font(font)
693 .text_shaping(self.text_shaping);
694
695 if let Some(text_size) = self.text_size {
696 menu = menu.text_size(text_size);
697 }
698
699 let anchor = prefer_down_when_menu_fits(
700 layout.position() + translation,
701 *viewport,
702 bounds.height,
703 resolved_menu_height(
704 self.menu_height,
705 self.intrinsic_menu_height(renderer),
706 viewport.height,
707 ),
708 );
709
710 Some(menu.overlay(
711 anchor.position,
712 *viewport,
713 anchor.target_height,
714 self.menu_height,
715 ))
716 } else {
717 None
718 }
719 }
720}
721
722impl<'a, T, L, V, Message, Renderer> From<Select<'a, T, L, V, Message, Renderer>>
723 for Element<'a, Message, Theme, Renderer>
724where
725 T: Clone + ToString + PartialEq + 'a,
726 L: Borrow<[T]> + 'a,
727 V: Borrow<T> + 'a,
728 Message: Clone + 'a,
729 Renderer: text::Renderer + geometry::Renderer + 'a,
730{
731 fn from(select: Select<'a, T, L, V, Message, Renderer>) -> Self {
732 Self::new(select)
733 }
734}
735
736#[derive(Debug)]
737struct State<P: text::Paragraph> {
738 menu: menu_overlay::State,
739 keyboard_modifiers: keyboard::Modifiers,
740 is_open: bool,
741 hovered_option: Option<usize>,
742 options: Vec<paragraph::Plain<P>>,
743 placeholder: paragraph::Plain<P>,
744 label: paragraph::Plain<P>,
745 handle_rotation: AnimatedScalar,
746 last_status: Option<Status>,
747}
748
749impl<P: text::Paragraph> State<P> {
750 fn new() -> Self {
751 Self {
752 menu: menu_overlay::State::default(),
753 keyboard_modifiers: keyboard::Modifiers::default(),
754 is_open: bool::default(),
755 hovered_option: Option::default(),
756 options: Vec::new(),
757 placeholder: paragraph::Plain::default(),
758 label: paragraph::Plain::default(),
759 handle_rotation: AnimatedScalar::new(menu_handle_rotation_target(false)),
760 last_status: None,
761 }
762 }
763
764 fn set_open(&mut self, is_open: bool, now: Instant) {
765 set_menu_open(
766 &mut self.is_open,
767 &mut self.handle_rotation,
768 &mut self.last_status,
769 is_open,
770 now,
771 );
772
773 if is_open {
774 self.menu.reverse_open(now);
775 } else {
776 self.menu.start_close(now);
777 }
778 }
779
780 fn is_animating(&self) -> bool {
781 self.handle_rotation.is_animating() || self.menu.is_animating()
782 }
783
784 fn advance(&mut self, now: Instant) -> bool {
785 self.handle_rotation.advance(now) | self.menu.advance(now)
786 }
787}
788
789fn select_status(is_open: bool, is_hovered: bool) -> Status {
790 if is_open {
791 Status::Opened { is_hovered }
792 } else if is_hovered {
793 Status::Hovered
794 } else {
795 Status::Active
796 }
797}
798
799fn select_label_color(theme: &Theme, status: Status) -> Color {
800 let colors = theme.colors();
801
802 match status {
803 Status::Opened { .. } => colors.primary.color,
804 Status::Hovered => colors.surface.text,
805 Status::Active => colors.surface.text_variant,
806 }
807}
808
809fn menu_handle_rotation_target(is_open: bool) -> f32 {
810 if is_open { 1.0 } else { 0.0 }
811}
812
813fn set_menu_open(
814 open_state: &mut bool,
815 handle_rotation: &mut AnimatedScalar,
816 last_status: &mut Option<Status>,
817 is_open: bool,
818 now: Instant,
819) {
820 let target = menu_handle_rotation_target(is_open);
821
822 let _ = handle_rotation.advance(now);
823
824 *open_state = is_open;
825 *last_status = None;
826 handle_rotation.set_target(
827 target,
828 now,
829 duration_ms(MENU_HANDLE_ROTATION_DURATION_MS),
830 tokens::motion::EASING_STANDARD,
831 );
832}
833
834fn draw_default_handle<Renderer>(
835 renderer: &mut Renderer,
836 center: Point,
837 size: f32,
838 progress: f32,
839 color: Color,
840) where
841 Renderer: geometry::Renderer,
842{
843 if size <= 0.0 {
844 return;
845 }
846
847 let top_left = Point::new(center.x - size / 2.0, center.y - size / 2.0);
848 let mut frame = Frame::new(renderer, Size::new(size, size));
849 let origin = Point::new(size / 2.0, size / 2.0);
850
851 frame.with_save(|frame| {
852 frame.translate(Vector::new(origin.x, origin.y));
853 frame.rotate(menu_handle_rotation_radians(progress));
854 frame.translate(Vector::new(-origin.x, -origin.y));
855 frame.fill(&default_handle_arrow_path(size), color);
856 });
857
858 renderer.with_translation(Vector::new(top_left.x, top_left.y), |renderer| {
859 renderer.draw_geometry(frame.into_geometry());
860 });
861}
862
863fn menu_handle_rotation_radians(progress: f32) -> f32 {
864 PI * progress.clamp(0.0, 1.0)
865}
866
867fn default_handle_arrow_path(size: f32) -> Path {
868 let [left, tip, right] = default_handle_arrow_points(size);
869
870 Path::new(|path| {
871 path.move_to(left);
872 path.line_to(tip);
873 path.line_to(right);
874 path.close();
875 })
876}
877
878fn default_handle_arrow_points(size: f32) -> [Point; 3] {
879 [
880 material_icon_point(MENU_HANDLE_ARROW_LEFT_X, MENU_HANDLE_ARROW_TOP_Y, size),
881 material_icon_point(MENU_HANDLE_ARROW_CENTER_X, MENU_HANDLE_ARROW_BOTTOM_Y, size),
882 material_icon_point(MENU_HANDLE_ARROW_RIGHT_X, MENU_HANDLE_ARROW_TOP_Y, size),
883 ]
884}
885
886fn material_icon_point(x: f32, y: f32, size: f32) -> Point {
887 Point::new(
888 x / MENU_HANDLE_VIEWPORT_SIZE * size,
889 y / MENU_HANDLE_VIEWPORT_SIZE * size,
890 )
891}
892
893impl<P: text::Paragraph> Default for State<P> {
894 fn default() -> Self {
895 Self::new()
896 }
897}
898
899fn find_next<'a, T: PartialEq>(
900 selected: &'a T,
901 mut options: impl Iterator<Item = &'a T>,
902) -> Option<&'a T> {
903 let _ = options.find(|&option| option == selected);
904
905 options.next()
906}
907
908pub(crate) fn menu_option_padding() -> Padding {
909 let vertical = (tokens::component::select::MENU_LIST_ITEM_CONTAINER_HEIGHT
910 - tokens::component::text_field::INPUT_TEXT_LINE_HEIGHT)
911 / 2.0;
912
913 Padding {
914 top: vertical,
915 right: tokens::component::text_field::TRAILING_SPACE,
916 bottom: vertical,
917 left: tokens::component::text_field::LEADING_SPACE,
918 }
919}
920
921pub(crate) fn material_menu_height(option_count: usize) -> Length {
922 let visible_options = option_count.clamp(1, MAX_VISIBLE_OPTIONS) as f32;
923
924 Length::Fixed(tokens::component::select::MENU_LIST_ITEM_CONTAINER_HEIGHT * visible_options)
925}
926
927pub(crate) fn resolved_menu_height(
928 menu_height: Length,
929 intrinsic_height: f32,
930 viewport_height: f32,
931) -> f32 {
932 match menu_height {
933 Length::Fixed(height) => height,
934 Length::Shrink => intrinsic_height,
935 Length::Fill | Length::FillPortion(_) => viewport_height,
936 }
937}
938
939pub(crate) fn prefer_down_when_menu_fits(
940 position: Point,
941 viewport: Rectangle,
942 target_height: f32,
943 menu_height: f32,
944) -> MenuAnchor {
945 let down_anchor_y = position.y + target_height;
946 let space_below = viewport.height - down_anchor_y;
947
948 if space_below < menu_height {
949 return MenuAnchor {
950 position,
951 target_height,
952 };
953 }
954
955 if space_below > position.y {
956 return MenuAnchor {
957 position,
958 target_height,
959 };
960 }
961
962 let adjusted_y = position.y.min((space_below - DIRECTION_EPSILON).max(0.0));
963
964 MenuAnchor {
965 position: Point::new(position.x, adjusted_y),
966 target_height: down_anchor_y - adjusted_y,
967 }
968}
969
970#[cfg(test)]
971#[path = "../../../tests/widget/component/select.rs"]
972mod tests;