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>).into();
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>).into();
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 state.menu.start_open(self.options.borrow().len(), now);
414
415 if let Some(on_open) = &self.on_open {
416 shell.publish(on_open.clone());
417 }
418
419 shell.capture_event();
420 }
421 }
422 Event::Mouse(mouse::Event::WheelScrolled {
423 delta: mouse::ScrollDelta::Lines { y, .. },
424 }) => {
425 if state.keyboard_modifiers.command()
426 && cursor.is_over(layout.bounds())
427 && !state.is_open
428 {
429 let options = self.options.borrow();
430 let selected = self.selected.as_ref().map(Borrow::borrow);
431
432 let next_option = if *y < 0.0 {
433 if let Some(selected) = selected {
434 find_next(selected, options.iter())
435 } else {
436 options.first()
437 }
438 } else if *y > 0.0 {
439 if let Some(selected) = selected {
440 find_next(selected, options.iter().rev())
441 } else {
442 options.last()
443 }
444 } else {
445 None
446 };
447
448 if let Some(next_option) = next_option {
449 shell.publish((self.on_select)(next_option.clone()));
450 }
451
452 shell.capture_event();
453 }
454 }
455 Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
456 state.keyboard_modifiers = *modifiers;
457 }
458 _ => {}
459 };
460
461 let now = match event {
462 Event::Window(window::Event::RedrawRequested(now)) => Some(*now),
463 _ => None,
464 };
465
466 if let Some(now) = now
467 && state.advance(now)
468 {
469 shell.request_redraw();
470 }
471
472 let status = select_status(state.is_open, cursor.is_over(layout.bounds()));
473
474 if state.last_status != Some(status) {
475 state.last_status = Some(status);
476 shell.request_redraw();
477 } else if state.is_animating() {
478 shell.request_redraw();
479 }
480 }
481
482 fn mouse_interaction(
483 &self,
484 _tree: &Tree,
485 layout: Layout<'_>,
486 cursor: mouse::Cursor,
487 _viewport: &Rectangle,
488 _renderer: &Renderer,
489 ) -> mouse::Interaction {
490 if cursor.is_over(layout.bounds()) {
491 mouse::Interaction::Pointer
492 } else {
493 mouse::Interaction::default()
494 }
495 }
496
497 fn draw(
498 &self,
499 tree: &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 font = self.font.unwrap_or_else(|| renderer.default_font());
508 let selected = self.selected.as_ref().map(Borrow::borrow);
509 let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
510
511 let bounds = layout.bounds();
512 let status = state
513 .last_status
514 .unwrap_or_else(|| select_status(state.is_open, cursor.is_over(bounds)));
515
516 let style = <Theme as iced_select::Catalog>::style(theme, &self.class, status);
517
518 let label_width = self
519 .label
520 .as_ref()
521 .map(|_| state.label.min_width())
522 .unwrap_or(0.0);
523 let label_x = bounds.x + tokens::component::text_field::LEADING_SPACE;
524 let label_notch = self.label.as_ref().and_then(|_| {
525 text_field_floating_label_notch(bounds, label_x, label_width, label_width, 1.0)
526 });
527
528 draw_text_field_outline(
529 renderer,
530 bounds,
531 style.background,
532 style.border,
533 label_notch,
534 );
535
536 let text_handle = match &self.handle {
537 Handle::Arrow { size } => {
538 let size = size.unwrap_or(Pixels(tokens::component::select::TRAILING_ICON_SIZE));
539 let right = bounds.x + bounds.width - self.field_padding.right;
540 let center = Point::new(right - size.0 / 2.0, bounds.center_y());
541
542 draw_default_handle(
543 renderer,
544 center,
545 size.0,
546 state.handle_rotation.value,
547 style.handle_color,
548 );
549
550 None
551 }
552 Handle::Static(Icon {
553 font,
554 code_point,
555 size,
556 line_height,
557 shaping,
558 }) => Some((*font, *code_point, *size, *line_height, *shaping)),
559 Handle::Dynamic { open, closed } => {
560 if state.is_open {
561 Some((
562 open.font,
563 open.code_point,
564 open.size,
565 open.line_height,
566 open.shaping,
567 ))
568 } else {
569 Some((
570 closed.font,
571 closed.code_point,
572 closed.size,
573 closed.line_height,
574 closed.shaping,
575 ))
576 }
577 }
578 Handle::None => None,
579 };
580
581 if let Some((font, code_point, size, line_height, shaping)) = text_handle {
582 let size = size.unwrap_or_else(|| renderer.default_size());
583
584 renderer.fill_text(
585 Text {
586 content: code_point.to_string(),
587 size,
588 line_height,
589 font,
590 bounds: Size::new(bounds.width, f32::from(line_height.to_absolute(size))),
591 align_x: text::Alignment::Right,
592 align_y: alignment::Vertical::Center,
593 shaping,
594 wrapping: text::Wrapping::default(),
595 },
596 Point::new(
597 bounds.x + bounds.width - self.field_padding.right,
598 bounds.center_y(),
599 ),
600 style.handle_color,
601 *viewport,
602 );
603 }
604
605 let label = selected.map(ToString::to_string);
606
607 if let Some(label) = label.or_else(|| self.placeholder.clone()) {
608 let text_size = self.text_size.unwrap_or_else(|| renderer.default_size());
609
610 renderer.fill_text(
611 Text {
612 content: label,
613 size: text_size,
614 line_height: self.text_line_height,
615 font,
616 bounds: Size::new(
617 bounds.width - self.field_padding.x(),
618 f32::from(self.text_line_height.to_absolute(text_size)),
619 ),
620 align_x: text::Alignment::Default,
621 align_y: alignment::Vertical::Center,
622 shaping: self.text_shaping,
623 wrapping: text::Wrapping::default(),
624 },
625 Point::new(bounds.x + self.field_padding.left, bounds.center_y()),
626 if selected.is_some() {
627 style.text_color
628 } else {
629 style.placeholder_color
630 },
631 *viewport,
632 );
633 }
634
635 if let Some(label) = &self.label {
636 let label_size = Pixels(tokens::component::text_field::LABEL_TEXT_POPULATED_SIZE);
637 let label_line_height = text::LineHeight::Absolute(Pixels(
638 tokens::component::text_field::LABEL_TEXT_POPULATED_LINE_HEIGHT,
639 ));
640 let label_height = f32::from(label_line_height.to_absolute(label_size));
641 let label_y = bounds.y;
642
643 renderer.fill_text(
644 Text {
645 content: label.clone(),
646 size: label_size,
647 line_height: label_line_height,
648 font,
649 bounds: Size::new(label_width, label_height),
650 align_x: text::Alignment::Default,
651 align_y: alignment::Vertical::Center,
652 shaping: self.text_shaping,
653 wrapping: text::Wrapping::None,
654 },
655 Point::new(label_x, label_y),
656 select_label_color(theme, status),
657 *viewport,
658 );
659 }
660 }
661
662 fn overlay<'b>(
663 &'b mut self,
664 tree: &'b mut Tree,
665 layout: Layout<'_>,
666 renderer: &Renderer,
667 viewport: &Rectangle,
668 translation: Vector,
669 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
670 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
671 let font = self.font.unwrap_or_else(|| renderer.default_font());
672
673 if state.is_open {
674 let bounds = layout.bounds();
675 let on_select = &self.on_select;
676 let menu_state = &mut state.menu;
677 let hovered_option = &mut state.hovered_option;
678 let open_state = &mut state.is_open;
679 let handle_rotation = &mut state.handle_rotation;
680 let last_status = &mut state.last_status;
681
682 let mut menu = menu_overlay::Menu::new(
683 menu_state,
684 self.options.borrow(),
685 hovered_option,
686 |option| {
687 let now = Instant::now();
688
689 set_menu_open(open_state, handle_rotation, last_status, false, now);
690
691 (on_select)(option)
692 },
693 None,
694 &self.menu_class,
695 )
696 .width(bounds.width)
697 .padding(self.option_padding)
698 .font(font)
699 .text_shaping(self.text_shaping);
700
701 if let Some(text_size) = self.text_size {
702 menu = menu.text_size(text_size);
703 }
704
705 let anchor = prefer_down_when_menu_fits(
706 layout.position() + translation,
707 *viewport,
708 bounds.height,
709 resolved_menu_height(
710 self.menu_height,
711 self.intrinsic_menu_height(renderer),
712 viewport.height,
713 ),
714 );
715
716 Some(menu.overlay(
717 anchor.position,
718 *viewport,
719 anchor.target_height,
720 self.menu_height,
721 ))
722 } else {
723 None
724 }
725 }
726}
727
728impl<'a, T, L, V, Message, Renderer> From<Select<'a, T, L, V, Message, Renderer>>
729 for Element<'a, Message, Theme, Renderer>
730where
731 T: Clone + ToString + PartialEq + 'a,
732 L: Borrow<[T]> + 'a,
733 V: Borrow<T> + 'a,
734 Message: Clone + 'a,
735 Renderer: text::Renderer + geometry::Renderer + 'a,
736{
737 fn from(select: Select<'a, T, L, V, Message, Renderer>) -> Self {
738 Self::new(select)
739 }
740}
741
742#[derive(Debug)]
743struct State<P: text::Paragraph> {
744 menu: menu_overlay::State,
745 keyboard_modifiers: keyboard::Modifiers,
746 is_open: bool,
747 hovered_option: Option<usize>,
748 options: Vec<paragraph::Plain<P>>,
749 placeholder: paragraph::Plain<P>,
750 label: paragraph::Plain<P>,
751 handle_rotation: AnimatedScalar,
752 last_status: Option<Status>,
753}
754
755impl<P: text::Paragraph> State<P> {
756 fn new() -> Self {
757 Self {
758 menu: menu_overlay::State::default(),
759 keyboard_modifiers: keyboard::Modifiers::default(),
760 is_open: bool::default(),
761 hovered_option: Option::default(),
762 options: Vec::new(),
763 placeholder: paragraph::Plain::default(),
764 label: paragraph::Plain::default(),
765 handle_rotation: AnimatedScalar::new(menu_handle_rotation_target(false)),
766 last_status: None,
767 }
768 }
769
770 fn set_open(&mut self, is_open: bool, now: Instant) {
771 set_menu_open(
772 &mut self.is_open,
773 &mut self.handle_rotation,
774 &mut self.last_status,
775 is_open,
776 now,
777 );
778 }
779
780 fn is_animating(&self) -> bool {
781 self.handle_rotation.is_animating()
782 }
783
784 fn advance(&mut self, now: Instant) -> bool {
785 self.handle_rotation.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;