1use std::cell::Cell;
2use std::rc::Rc;
3
4use sequoia_assets::{AssetRegistry, icon_key};
5use sequoia_core::{Color, Edges, Length, Rect, Size};
6use sequoia_input::{CursorIcon, Event, EventPhase, Key, PointerButton, PointerEvent};
7use sequoia_layout::{Align, Axis, ContainerStyle, LayoutCtx, LayoutStyle};
8use sequoia_reactive::Signal;
9use sequoia_style::{ButtonStyle, Theme, VisualState};
10use sequoia_text::FontWeight;
11use sequoia_view::{
12 Anchor, AnyView, Dismiss, EventCtx, OverlayEntry, OverlayId, OverlayLayer, PaintCtx, Placement,
13 Unit, View,
14};
15
16const PAD_X: f32 = 10.0;
17
18const PAD_Y: f32 = 5.0;
19
20const CHEV_GAP: f32 = 8.0;
21
22const CHEV_W: f32 = 10.0;
23
24const CHEV_H: f32 = 6.0;
25
26const ITEM_H: f32 = 28.0;
27
28const ITEM_PAD_X: f32 = 12.0;
29
30const MENU_RADIUS: f32 = 4.0;
31
32pub struct DropdownButton {
33 selected: Signal<usize>,
34 options: Vec<String>,
35 placeholder: String,
36 style: Option<ButtonStyle>,
37 background: Option<Color>,
38 hover: Option<Color>,
39 pressed: Option<Color>,
40 disabled_background: Option<Color>,
41 text_color: Option<Color>,
42 text_disabled: Option<Color>,
43 font_size: Option<f32>,
44 font_weight: Option<u16>,
45 radius: Option<f32>,
46 padding: Option<(f32, f32)>,
47 border_width: Option<f32>,
48 border_color: Option<Color>,
49 menu_background: Option<Color>,
50 menu_border: Option<Color>,
51 item_height: f32,
52 disabled: bool,
53 on_select: Option<Rc<dyn Fn(usize)>>,
54
55 chevron_icon: Option<String>,
56
57 chevron_open_icon: Option<String>,
58
59 chevron_size: Option<f32>,
60
61 chevron_color: Option<Color>,
62
63 open: Signal<bool>,
64 highlight: Signal<usize>,
65 overlay: Rc<Cell<Option<OverlayId>>>,
66 margin: Edges,
67 min_width: Option<f32>,
68 max_width: Option<f32>,
69 min_height: Option<f32>,
70 max_height: Option<f32>,
71}
72
73impl DropdownButton {
74 pub fn new(selected: Signal<usize>, options: Vec<impl Into<String>>) -> Self {
75 Self {
76 selected,
77 options: options.into_iter().map(Into::into).collect(),
78 placeholder: String::new(),
79 style: None,
80 background: None,
81 hover: None,
82 pressed: None,
83 disabled_background: None,
84 text_color: None,
85 text_disabled: None,
86 font_size: None,
87 font_weight: None,
88 radius: None,
89 padding: None,
90 border_width: None,
91 border_color: None,
92 menu_background: None,
93 menu_border: None,
94 item_height: ITEM_H,
95 disabled: false,
96 on_select: None,
97 chevron_icon: None,
98 chevron_open_icon: None,
99 chevron_size: None,
100 chevron_color: None,
101 open: Signal::new(false),
102 highlight: Signal::new(0),
103 overlay: Rc::new(Cell::new(None)),
104 margin: Edges::ZERO,
105 min_width: None,
106 max_width: None,
107 min_height: None,
108 max_height: None,
109 }
110 }
111
112 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
113 self.placeholder = placeholder.into();
114 self
115 }
116
117 pub fn style(mut self, style: ButtonStyle) -> Self {
118 self.style = Some(style);
119 self
120 }
121
122 pub fn background(mut self, color: Color) -> Self {
123 self.background = Some(color);
124 self
125 }
126
127 pub fn hover(mut self, color: Color) -> Self {
128 self.hover = Some(color);
129 self
130 }
131
132 pub fn pressed(mut self, color: Color) -> Self {
133 self.pressed = Some(color);
134 self
135 }
136
137 pub fn disabled_background(mut self, color: Color) -> Self {
138 self.disabled_background = Some(color);
139 self
140 }
141
142 pub fn text_color(mut self, color: Color) -> Self {
143 self.text_color = Some(color);
144 self
145 }
146
147 pub fn text_disabled(mut self, color: Color) -> Self {
148 self.text_disabled = Some(color);
149 self
150 }
151
152 pub fn font_size(mut self, size: f32) -> Self {
153 self.font_size = Some(size);
154 self
155 }
156
157 pub fn font_weight(mut self, weight: u16) -> Self {
158 self.font_weight = Some(weight);
159 self
160 }
161
162 pub fn radius(mut self, radius: f32) -> Self {
163 self.radius = Some(radius);
164 self
165 }
166
167 pub fn padding_xy(mut self, pad_x: f32, pad_y: f32) -> Self {
168 self.padding = Some((pad_x, pad_y));
169 self
170 }
171
172 pub fn border_width(mut self, width: f32) -> Self {
173 self.border_width = Some(width);
174 self
175 }
176
177 pub fn border_color(mut self, color: Color) -> Self {
178 self.border_color = Some(color);
179 self
180 }
181
182 pub fn border(mut self, width: f32, color: Color) -> Self {
183 self.border_width = Some(width);
184 self.border_color = Some(color);
185 self
186 }
187
188 pub fn menu_background(mut self, color: Color) -> Self {
189 self.menu_background = Some(color);
190 self
191 }
192
193 pub fn menu_border(mut self, color: Color) -> Self {
194 self.menu_border = Some(color);
195 self
196 }
197
198 pub fn item_height(mut self, height: f32) -> Self {
199 self.item_height = height.max(8.0);
200 self
201 }
202
203 pub fn disabled(mut self, disabled: bool) -> Self {
204 self.disabled = disabled;
205 self
206 }
207
208 pub fn min_width(mut self, v: f32) -> Self {
209 self.min_width = Some(v.max(0.0));
210 self
211 }
212
213 pub fn max_width(mut self, v: f32) -> Self {
214 self.max_width = Some(v.max(0.0));
215 self
216 }
217
218 pub fn min_height(mut self, v: f32) -> Self {
219 self.min_height = Some(v.max(0.0));
220 self
221 }
222
223 pub fn max_height(mut self, v: f32) -> Self {
224 self.max_height = Some(v.max(0.0));
225 self
226 }
227
228 pub fn on_select(mut self, f: impl Fn(usize) + 'static) -> Self {
229 self.on_select = Some(Rc::new(f));
230 self
231 }
232
233 pub fn chevron_icon(mut self, name: impl Into<String>) -> Self {
234 self.chevron_icon = Some(name.into());
235 self
236 }
237
238 pub fn chevron_open_icon(mut self, name: impl Into<String>) -> Self {
239 self.chevron_open_icon = Some(name.into());
240 self
241 }
242
243 pub fn disclosure_icons(
244 mut self,
245 collapsed: impl Into<String>,
246 expanded: impl Into<String>,
247 ) -> Self {
248 self.chevron_icon = Some(collapsed.into());
249 self.chevron_open_icon = Some(expanded.into());
250 self
251 }
252
253 pub fn chevron_size(mut self, px: f32) -> Self {
254 self.chevron_size = Some(px.max(1.0));
255 self
256 }
257
258 pub fn chevron_color(mut self, color: Color) -> Self {
259 self.chevron_color = Some(color);
260 self
261 }
262
263 pub fn margin(mut self, margin: impl Into<Edges>) -> Self {
264 let m = margin.into();
265 self.margin = Edges::only(
266 sanitize_margin(m.left),
267 sanitize_margin(m.top),
268 sanitize_margin(m.right),
269 sanitize_margin(m.bottom),
270 );
271 self
272 }
273
274 pub fn margin_xy(self, x: f32, y: f32) -> Self {
275 self.margin(Edges::xy(x, y))
276 }
277
278 pub fn margin_x(mut self, x: f32) -> Self {
279 let x = sanitize_margin(x);
280 self.margin.left = x;
281 self.margin.right = x;
282 self
283 }
284
285 pub fn margin_y(mut self, y: f32) -> Self {
286 let y = sanitize_margin(y);
287 self.margin.top = y;
288 self.margin.bottom = y;
289 self
290 }
291
292 pub fn margin_left(mut self, v: f32) -> Self {
293 self.margin.left = sanitize_margin(v);
294 self
295 }
296
297 pub fn margin_right(mut self, v: f32) -> Self {
298 self.margin.right = sanitize_margin(v);
299 self
300 }
301
302 pub fn margin_top(mut self, v: f32) -> Self {
303 self.margin.top = sanitize_margin(v);
304 self
305 }
306
307 pub fn margin_bottom(mut self, v: f32) -> Self {
308 self.margin.bottom = sanitize_margin(v);
309 self
310 }
311
312 pub fn is_open(&self) -> bool {
313 self.open.get_untracked()
314 }
315
316 fn open_state(&self) -> Signal<bool> {
317 self.open.clone()
318 }
319
320 fn highlight_state(&self) -> Signal<usize> {
321 self.highlight.clone()
322 }
323
324 fn take_overlay(&self) -> Option<OverlayId> {
325 self.overlay.take()
326 }
327
328 fn display(&self) -> String {
329 self.options
330 .get(self.selected.get_untracked())
331 .cloned()
332 .unwrap_or_else(|| self.placeholder.clone())
333 }
334
335 fn showing_placeholder(&self) -> bool {
336 self.options.get(self.selected.get_untracked()).is_none()
337 }
338
339 fn open(&self) {
340 if self.disabled || self.options.is_empty() {
341 return;
342 }
343 let sel = self.selected.get_untracked().min(self.options.len() - 1);
344 self.highlight_state().set(sel);
345 self.open_state().set(true);
346 }
347
348 fn close_with(&self, ctx: &mut EventCtx) {
349 self.open_state().set(false);
350 if let Some(id) = self.take_overlay() {
351 ctx.hide_overlay(id);
352 }
353 }
354
355 fn select_with(&self, idx: usize, ctx: &mut EventCtx) {
356 if idx >= self.options.len() {
357 return;
358 }
359 self.selected.set(idx);
360 self.highlight_state().set(idx);
361 if let Some(f) = &self.on_select {
362 f(idx);
363 }
364 self.close_with(ctx);
365 ctx.request_focus();
366 }
367
368 fn resolved_font_size(&self) -> f32 {
369 self.font_size
370 .or_else(|| self.style.and_then(|s| s.font_size))
371 .unwrap_or(14.0)
372 }
373
374 fn resolved_weight(&self) -> FontWeight {
375 self.font_weight
376 .or_else(|| self.style.and_then(|s| s.font_weight))
377 .map(FontWeight::new)
378 .unwrap_or_default()
379 }
380
381 fn resolved_padding(&self) -> (f32, f32) {
382 self.padding
383 .or_else(|| self.style.and_then(|s| s.padding))
384 .unwrap_or((PAD_X, PAD_Y))
385 }
386}
387
388struct ResolvedButton {
389 bg: Color,
390 text: Color,
391 font_size: f32,
392 weight: FontWeight,
393 radius: f32,
394 border_width: f32,
395 border_color: Color,
396}
397
398impl DropdownButton {
399 fn resolve(&self, theme: &Theme, state: VisualState) -> ResolvedButton {
400 let style = self.style.unwrap_or_default();
401
402 let bg = if self.disabled || state.disabled {
403 self.disabled_background
404 .or(style.disabled)
405 .unwrap_or(theme.disabled)
406 } else if state.pressed {
407 self.pressed
408 .or(style.pressed)
409 .unwrap_or(Color::hex(0x3A3A3A))
410 } else if state.hovered {
411 self.hover.or(style.hover).unwrap_or(Color::hex(0x323232))
412 } else {
413 self.background
414 .or(style.background)
415 .unwrap_or(Color::hex(0x2A2A2A))
416 };
417 let placeholder = self.showing_placeholder();
418 let text = if self.disabled || state.disabled {
419 self.text_disabled
420 .or(style.text_disabled)
421 .unwrap_or(theme.text_secondary)
422 } else if placeholder {
423 self.text_color
424 .or(style.text)
425 .unwrap_or(theme.text_secondary)
426 } else {
427 self.text_color.or(style.text).unwrap_or(theme.text)
428 };
429 ResolvedButton {
430 bg,
431 text,
432 font_size: self
433 .font_size
434 .or(style.font_size)
435 .unwrap_or(theme.font_size),
436 weight: self
437 .font_weight
438 .or(style.font_weight)
439 .map(FontWeight::new)
440 .unwrap_or_default(),
441 radius: self.radius.or(style.radius).unwrap_or(theme.radius),
442 border_width: self
443 .border_width
444 .or(style.border_width)
445 .unwrap_or(1.0)
446 .max(0.0),
447 border_color: self
448 .border_color
449 .or(style.border_color)
450 .unwrap_or(theme.border),
451 }
452 }
453
454 fn sync_overlay(&self, ctx: &mut PaintCtx, theme: &Theme) {
455 let open = self.open_state();
456 if self.disabled {
457 if open.get_untracked() {
458 open.set(false);
459 }
460 if let Some(id) = self.take_overlay() {
461 ctx.hide_overlay(id);
462 }
463 return;
464 }
465 if !open.get_untracked() {
466 if let Some(id) = self.take_overlay() {
467 ctx.hide_overlay(id);
468 }
469 return;
470 }
471 if self.options.is_empty() {
472 open.set(false);
473 return;
474 }
475 if let Some(id) = self.overlay.get() {
476 if !ctx.has_overlay(id) {
477 self.take_overlay();
478 open.set(false);
479 }
480 return;
481 }
482 let menu = DropdownMenu {
483 overlay: Rc::clone(&self.overlay),
484 options: self.options.clone(),
485 selected: self.selected.clone(),
486 open: open.clone(),
487 highlight: self.highlight_state(),
488 font_size: self.resolved_font_size(),
489 weight: self.resolved_weight(),
490 item_height: self.item_height,
491 background: self.menu_background.unwrap_or(theme.surface),
492 border: self.menu_border.unwrap_or(theme.border),
493 radius: MENU_RADIUS,
494
495 menu_min_width: ctx.rect.size.width.max(0.0),
496 on_select: self.on_select.clone(),
497 };
498
499 let id = ctx.show_overlay(
500 OverlayEntry::new(menu)
501 .anchor(Anchor::View(ctx.element))
502 .placement(Placement::Bottom)
503 .layer(OverlayLayer::Popup)
504 .dismiss(Dismiss::FocusLost)
505 .offset(0.0, 4.0),
506 );
507 self.overlay.set(Some(id));
508 }
509}
510
511impl View for DropdownButton {
512 fn reuse_state(&self, old: &Self) {
513 self.open.set(old.open.get_untracked());
514 self.highlight.set(old.highlight.get_untracked());
515 self.overlay.set(old.overlay.get());
516 }
517
518 fn body(&self) -> impl View + 'static {
519 Unit
520 }
521 fn is_leaf(&self) -> bool {
522 true
523 }
524 fn focusable(&self) -> bool {
525 !self.disabled
526 }
527 fn cursor_icon(&self, _state: VisualState) -> CursorIcon {
528 if self.disabled {
529 CursorIcon::Default
530 } else {
531 CursorIcon::Pointer
532 }
533 }
534 fn has_visual_state(&self) -> bool {
535 true
536 }
537 fn keyed_children(&self) -> Option<Vec<(u64, AnyView)>> {
538 let _ = self.selected.get();
539 let _ = self.open_state().get();
540 let _ = self.highlight_state().get();
541 Some(Vec::new())
542 }
543 fn layout_style(&self) -> LayoutStyle {
544 let mut style = LayoutStyle {
545 width: Length::Fit,
546 height: Length::Fit,
547 margin: self.margin,
548 ..LayoutStyle::default()
549 };
550 if let Some(v) = self.min_width {
551 style = style.min_width(v);
552 }
553 if let Some(v) = self.max_width {
554 style = style.max_width(v);
555 }
556 if let Some(v) = self.min_height {
557 style = style.min_height(v);
558 }
559 if let Some(v) = self.max_height {
560 style = style.max_height(v);
561 }
562 style
563 }
564 fn intrinsic_size(&self, ctx: &mut LayoutCtx) -> Option<Size> {
565 let size = self.resolved_font_size();
566 let weight = self.resolved_weight();
567 let (pad_x, pad_y) = self.resolved_padding();
568
569 let mut w = ctx.text_width_weighted(&self.placeholder, size, weight);
570 for opt in &self.options {
571 w = w.max(ctx.text_width_weighted(opt, size, weight));
572 }
573 Some(Size::new(
574 w + CHEV_GAP + CHEV_W + pad_x * 2.0,
575 LayoutCtx::text_line_height(size) + pad_y * 2.0,
576 ))
577 }
578 fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
579 if self.disabled {
580 return;
581 }
582 let open = self.open_state();
583 let is_open = open.get_untracked();
584 match event {
585 Event::Pointer(PointerEvent::Up {
586 button: PointerButton::Left,
587 ..
588 }) => {
589 if ctx.phase != EventPhase::Target {
590 return;
591 }
592 if ctx.state.pressed && ctx.hover_self {
593 if is_open {
594 open.set(false);
595 if let Some(id) = self.take_overlay() {
596 ctx.hide_overlay(id);
597 }
598 } else {
599 self.open();
600 }
601 ctx.request_focus();
602 ctx.handled();
603 }
604 }
605 Event::Key(key) => {
606 if ctx.phase != EventPhase::Target || !ctx.state.focused {
607 return;
608 }
609 if self.options.is_empty() {
610 return;
611 }
612 let len = self.options.len();
613 let hl = self.highlight_state().get_untracked().min(len - 1);
614 match key.key {
615 Key::ESCAPE => {
616 if is_open {
617 open.set(false);
618 if let Some(id) = self.take_overlay() {
619 ctx.hide_overlay(id);
620 }
621 ctx.handled();
622 }
623 }
624 Key::ENTER | Key::SPACE => {
625 if is_open {
626 self.select_with(hl, ctx);
627 } else {
628 self.open();
629 }
630 ctx.handled();
631 }
632 Key::ARROW_DOWN => {
633 if is_open {
634 self.highlight_state().set((hl + 1).min(len - 1));
635 } else {
636 self.open();
637 }
638 ctx.handled();
639 }
640 Key::ARROW_UP => {
641 if is_open {
642 self.highlight_state().set(hl.saturating_sub(1));
643 } else {
644 self.open();
645 }
646 ctx.handled();
647 }
648 Key::HOME => {
649 if is_open {
650 self.highlight_state().set(0);
651 ctx.handled();
652 }
653 }
654 Key::END if is_open => {
655 self.highlight_state().set(len - 1);
656 ctx.handled();
657 }
658 _ => {}
659 }
660 }
661 _ => {}
662 }
663 }
664 fn on_unmount(&self) {
665 self.overlay.set(None);
666 }
667 fn paint_self(&self, ctx: &mut PaintCtx) {
668 let theme = ctx.theme();
669 let mut vs = ctx.state;
670 vs.disabled = self.disabled;
671
672 if self.open_state().get_untracked() {
673 vs.pressed = true;
674 }
675 let r = self.resolve(&theme, vs);
676 if r.border_width > 0.0 && r.border_color.a > 0.0 {
677 ctx.bordered_quad(ctx.rect, r.bg, r.radius, r.border_width, r.border_color);
678 } else {
679 ctx.quad(ctx.rect, r.bg, r.radius);
680 }
681
682 let label = self.display();
683 let (pad_x, _) = self.resolved_padding();
684 let x = ctx.rect.left() + pad_x;
685 let y = ctx.rect.top() + ctx.rect.size.height * 0.5 + r.font_size * 0.35;
686 ctx.draw_text_weighted(&label, (x, y), r.font_size, r.weight, r.text);
687
688 let cx = ctx.rect.right() - pad_x - CHEV_W;
689 let cy = ctx.rect.top() + ctx.rect.size.height * 0.5;
690 let hw = CHEV_W * 0.5;
691 let hh = CHEV_H * 0.5;
692
693 let is_open = self.open_state().get_untracked();
694 let icon_name: Option<&str> = if is_open {
695 self.chevron_open_icon
696 .as_deref()
697 .or(self.chevron_icon.as_deref())
698 } else {
699 self.chevron_icon.as_deref()
700 };
701 let mut painted_icon = false;
702 if let Some(name) = icon_name {
703 let px = self.chevron_size.unwrap_or(10.0);
704 let color = self.chevron_color.unwrap_or(r.text);
705 if let Some(assets) = ctx.environment.get::<AssetRegistry>() {
706 let raster_px = ((px * ctx.scale()).round() as u32).max(1);
707 if let Ok(sprite) = assets.rasterize_icon(name, raster_px) {
708 let dst = Rect::from_xywh(cx + hw - px * 0.5, cy - px * 0.5, px, px);
709 ctx.draw_sprite(
710 icon_key(name, raster_px),
711 sprite.width,
712 sprite.height,
713 &sprite.alpha,
714 dst,
715 color,
716 );
717 painted_icon = true;
718 }
719 }
720 }
721 if !painted_icon {
722 ctx.polygon(
723 &[
724 sequoia_core::Point::new(cx, cy - hh),
725 sequoia_core::Point::new(cx + CHEV_W, cy - hh),
726 sequoia_core::Point::new(cx + hw, cy + hh),
727 ],
728 r.text,
729 );
730 }
731
732 self.sync_overlay(ctx, &theme);
733 }
734}
735
736struct DropdownMenu {
737 overlay: Rc<Cell<Option<OverlayId>>>,
738 options: Vec<String>,
739 selected: Signal<usize>,
740 open: Signal<bool>,
741 highlight: Signal<usize>,
742 font_size: f32,
743 weight: FontWeight,
744 item_height: f32,
745 background: Color,
746 border: Color,
747 radius: f32,
748 menu_min_width: f32,
749 on_select: Option<Rc<dyn Fn(usize)>>,
750}
751
752impl View for DropdownMenu {
753 fn body(&self) -> impl View + 'static {
754 Unit
755 }
756 fn children_override(&self) -> Option<Vec<AnyView>> {
757 Some(
758 self.options
759 .iter()
760 .enumerate()
761 .map(|(i, label)| {
762 AnyView::new(DropdownItem {
763 overlay: Rc::clone(&self.overlay),
764 index: i,
765 label: label.clone(),
766 selected: self.selected.clone(),
767 open: self.open.clone(),
768 highlight: self.highlight.clone(),
769 font_size: self.font_size,
770 weight: self.weight,
771 item_height: self.item_height,
772 on_select: self.on_select.clone(),
773 })
774 })
775 .collect(),
776 )
777 }
778 fn layout_style(&self) -> LayoutStyle {
779 LayoutStyle {
780 width: Length::Fit,
781 height: Length::Fit,
782 ..LayoutStyle::default().min_width(self.menu_min_width.max(0.0))
783 }
784 }
785 fn container(&self) -> Option<ContainerStyle> {
786 Some(ContainerStyle {
787 axis: Axis::Vertical,
788 gap: 0.0,
789 cross_gap: 0.0,
790 align: Align::Stretch,
791 justify: sequoia_layout::Justify::Start,
792 wrap: false,
793 align_content: Align::Start,
794 })
795 }
796 fn paint_self(&self, ctx: &mut PaintCtx) {
797 let rect = ctx.rect;
798 if rect.size.width <= 0.0 || rect.size.height <= 0.0 {
799 return;
800 }
801 ctx.shadow(
802 rect,
803 self.radius,
804 16.0,
805 sequoia_core::Point::new(0.0, 4.0),
806 Color::rgba(0.0, 0.0, 0.0, 0.3),
807 );
808 ctx.bordered_quad(rect, self.background, self.radius, 1.0, self.border);
809 }
810}
811
812struct DropdownItem {
813 overlay: Rc<Cell<Option<OverlayId>>>,
814 index: usize,
815 label: String,
816 selected: Signal<usize>,
817 open: Signal<bool>,
818 highlight: Signal<usize>,
819 font_size: f32,
820 weight: FontWeight,
821 item_height: f32,
822 on_select: Option<Rc<dyn Fn(usize)>>,
823}
824
825impl DropdownItem {
826 fn select(&self, ctx: &mut EventCtx) {
827 self.selected.set(self.index);
828 self.highlight.set(self.index);
829 if let Some(f) = &self.on_select {
830 f(self.index);
831 }
832 self.open.set(false);
833 if let Some(id) = self.overlay.take() {
834 ctx.hide_overlay(id);
835 }
836 ctx.handled();
837 }
838}
839
840impl View for DropdownItem {
841 fn body(&self) -> impl View + 'static {
842 Unit
843 }
844 fn is_leaf(&self) -> bool {
845 true
846 }
847 fn has_visual_state(&self) -> bool {
848 true
849 }
850 fn captures_pointer(&self) -> bool {
851 true
852 }
853 fn keyed_children(&self) -> Option<Vec<(u64, AnyView)>> {
854 let _ = self.selected.get();
855 let _ = self.highlight.get();
856 Some(Vec::new())
857 }
858 fn layout_style(&self) -> LayoutStyle {
859 LayoutStyle {
860 width: Length::Fit,
861 height: Length::Px(self.item_height),
862 ..LayoutStyle::default()
863 }
864 }
865 fn intrinsic_size(&self, ctx: &mut LayoutCtx) -> Option<Size> {
866 let w = ctx.text_width_weighted(&self.label, self.font_size, self.weight);
867 Some(Size::new(w + ITEM_PAD_X * 2.0, self.item_height))
868 }
869 fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
870 if ctx.phase != EventPhase::Target {
871 return;
872 }
873 match event {
874 Event::Pointer(PointerEvent::Up {
875 button: PointerButton::Left,
876 ..
877 }) => {
878 if ctx.state.pressed && ctx.hover_self {
879 self.select(ctx);
880 }
881 }
882 Event::Pointer(PointerEvent::Move { .. }) => {
883 if ctx.hover_self {
884 let cur = self.highlight.get_untracked();
885 if cur != self.index {
886 self.highlight.set(self.index);
887 }
888 }
889 }
890 Event::Key(key) if key.key == Key::ESCAPE => {
891 self.open.set(false);
892 if let Some(id) = self.overlay.take() {
893 ctx.hide_overlay(id);
894 }
895 ctx.handled();
896 }
897 _ => {}
898 }
899 }
900 fn paint_self(&self, ctx: &mut PaintCtx) {
901 let theme = ctx.theme();
902 let rect = ctx.rect;
903 if rect.size.width <= 0.0 || rect.size.height <= 0.0 {
904 return;
905 }
906 let is_selected = self.selected.get_untracked() == self.index;
907 let hl = self.highlight.get_untracked() == self.index;
908
909 if ctx.state.hovered || hl {
910 ctx.quad(
911 Rect::from_xywh(
912 rect.left() + 2.0,
913 rect.top(),
914 (rect.size.width - 4.0).max(0.0),
915 rect.size.height,
916 ),
917 Color::hex(0x4A4A4A),
918 4.0,
919 );
920 } else if is_selected {
921 ctx.quad(
922 Rect::from_xywh(
923 rect.left() + 2.0,
924 rect.top(),
925 (rect.size.width - 4.0).max(0.0),
926 rect.size.height,
927 ),
928 Color::hex(0x3A3A3A),
929 4.0,
930 );
931 }
932 let weight = if is_selected {
933 FontWeight::SEMIBOLD
934 } else {
935 self.weight
936 };
937 let x = rect.left() + ITEM_PAD_X;
938 let y = rect.top() + rect.size.height * 0.5 + self.font_size * 0.35;
939 ctx.draw_text_weighted(&self.label, (x, y), self.font_size, weight, theme.text);
940 }
941}
942
943fn sanitize_margin(v: f32) -> f32 {
944 if v.is_finite() { v.max(0.0) } else { 0.0 }
945}