1#![allow(non_snake_case)]
2
3use std::cell::{Cell, RefCell};
4use std::rc::Rc;
5use std::sync::atomic::{AtomicU64, Ordering};
6use web_time::Duration;
7
8use repose_core::NestedScrollConnection;
9use repose_core::animation::{AnimationSpec, CubicBezier, Easing, KeyframesSpec, RepeatableSpec};
10use repose_core::*;
11use repose_ui::anim::{animate_color, animate_f32};
12use repose_ui::textfield::{TextMeasureConfig, measure_text};
13use repose_ui::TextFieldConfig as BasicTextFieldConfig;
14use repose_ui::{BasicTextField, Box, Column, Row, Text, TextFieldState, TextStyle, ViewExt};
15
16use super::*;
17
18use crate::ripple::{RippleConfig, ripple};
19use crate::{Icon, Symbol};
20
21fn lerp_color(a: Color, b: Color, t: f32) -> Color {
22 let t = t.clamp(0.0, 1.0);
23 Color(
24 (a.0 as f32 + (b.0 as f32 - a.0 as f32) * t)
25 .round()
26 .clamp(0.0, 255.0) as u8,
27 (a.1 as f32 + (b.1 as f32 - a.1 as f32) * t)
28 .round()
29 .clamp(0.0, 255.0) as u8,
30 (a.2 as f32 + (b.2 as f32 - a.2 as f32) * t)
31 .round()
32 .clamp(0.0, 255.0) as u8,
33 (a.3 as f32 + (b.3 as f32 - a.3 as f32) * t)
34 .round()
35 .clamp(0.0, 255.0) as u8,
36 )
37}
38
39#[derive(Clone, Copy, Debug)]
41pub struct TopAppBarColors {
42 pub container_color: Color,
43 pub scrolled_container_color: Color,
44 pub navigation_icon_content_color: Color,
45 pub title_content_color: Color,
46 pub subtitle_content_color: Color,
47 pub action_icon_content_color: Color,
48}
49
50impl TopAppBarColors {
51 pub fn container_color(&self, scroll_fraction: f32) -> Color {
52 lerp_color(
53 self.container_color,
54 self.scrolled_container_color,
55 scroll_fraction.clamp(0.0, 1.0),
56 )
57 }
58}
59
60impl Default for TopAppBarColors {
61 fn default() -> Self {
62 Self {
63 container_color: TopAppBarDefaults::container_color(),
64 scrolled_container_color: TopAppBarDefaults::scrolled_container_color(),
65 navigation_icon_content_color: TopAppBarDefaults::navigation_icon_content_color(),
66 title_content_color: TopAppBarDefaults::title_content_color(),
67 subtitle_content_color: TopAppBarDefaults::subtitle_content_color(),
68 action_icon_content_color: TopAppBarDefaults::action_icon_content_color(),
69 }
70 }
71}
72
73#[derive(Clone, Copy, Debug, PartialEq)]
75pub enum TopAppBarScrollMode {
76 Pinned,
78 EnterAlways,
80}
81
82pub struct TopAppBarScrollBehavior {
89 pub collapsed_offset: Signal<f32>,
90 pub height: f32,
91 pub collapsed_height: f32,
92 pub mode: TopAppBarScrollMode,
93 _pending: Rc<Cell<f32>>,
94}
95
96impl TopAppBarScrollBehavior {
97 pub fn new(height: f32, collapsed_height: f32, mode: TopAppBarScrollMode) -> Self {
98 Self {
99 collapsed_offset: signal(0.0),
100 height,
101 collapsed_height,
102 mode,
103 _pending: Rc::new(Cell::new(0.0)),
104 }
105 }
106
107 pub fn nested_scroll_connection(&self) -> NestedScrollConnection {
110 let off = self.collapsed_offset.clone();
111 let max_collapse = -(self.height - self.collapsed_height);
112 let mode = self.mode;
113
114 NestedScrollConnection::new().on_pre_scroll(move |d: Vec2, _source| -> Vec2 {
115 if mode == TopAppBarScrollMode::Pinned {
116 return Vec2::ZERO;
117 }
118 let current = off.get();
119 if d.y > 0.0 {
120 if current <= max_collapse {
122 return Vec2::ZERO;
123 }
124 let collapse_room = current - max_collapse;
125 let consume = d.y.min(collapse_room);
126 off.set(current - consume);
127 repose_core::request_frame();
128 Vec2 { x: 0.0, y: consume }
129 } else {
130 if current >= 0.0 {
132 return Vec2::ZERO;
133 }
134 let expansion_room = -current;
135 let consume = (-d.y).min(expansion_room);
136 off.set(current + consume);
137 repose_core::request_frame();
138 Vec2 { x: 0.0, y: consume }
139 }
140 })
141 }
142
143 pub fn offset(&self) -> f32 {
145 self.collapsed_offset.get()
146 }
147}
148
149#[derive(Clone, Debug)]
151pub struct TopAppBarConfig {
152 pub modifier: Modifier,
153 pub colors: TopAppBarColors,
154 pub height: f32,
155 pub scroll_fraction: f32,
156 pub scroll_offset: f32,
159 pub window_insets: WindowInsets,
160 pub content_padding: PaddingValues,
161}
162
163#[derive(Clone, Copy, Debug)]
165pub struct WindowInsets {
166 pub top: f32,
167 pub bottom: f32,
168 pub left: f32,
169 pub right: f32,
170}
171
172impl Default for WindowInsets {
173 fn default() -> Self {
174 Self {
175 top: 0.0,
176 bottom: 0.0,
177 left: 0.0,
178 right: 0.0,
179 }
180 }
181}
182
183impl Default for TopAppBarConfig {
184 fn default() -> Self {
185 Self {
186 modifier: Modifier::new(),
187 colors: TopAppBarColors::default(),
188 height: TopAppBarDefaults::HEIGHT,
189 scroll_fraction: 0.0,
190 scroll_offset: 0.0,
191 window_insets: WindowInsets::default(),
192 content_padding: PaddingValues {
193 left: 4.0,
194 right: 4.0,
195 top: 0.0,
196 bottom: 0.0,
197 },
198 }
199 }
200}
201
202fn top_app_bar_layout(
203 title: View,
204 subtitle: Option<View>,
205 navigation_icon: Option<View>,
206 actions: Vec<View>,
207 config: TopAppBarConfig,
208 centered: bool,
209) -> View {
210 let insets = config.window_insets;
211 let bg = config.colors.container_color(config.scroll_fraction);
212 let mut m = Modifier::new()
213 .min_width(200.0)
214 .height(config.height + insets.top)
215 .background(bg)
216 .translate(0.0, config.scroll_offset)
217 .padding_values(PaddingValues {
218 left: config.content_padding.left + insets.left,
219 right: config.content_padding.right + insets.right,
220 top: config.content_padding.top + insets.top,
221 bottom: config.content_padding.bottom + insets.bottom,
222 })
223 .align_items(AlignItems::CENTER)
224 .then(config.modifier);
225 if centered {
226 m = m.justify_content(JustifyContent::CENTER);
227 }
228 Row(m).child((
229 navigation_icon.unwrap_or(Box(Modifier::new().width(16.0).fill_max_height())),
230 Box(Modifier::new()
231 .padding_values(PaddingValues {
232 left: 16.0,
233 right: 0.0,
234 top: 0.0,
235 bottom: 0.0,
236 })
237 .flex_grow(1.0))
238 .child(
239 Column(Modifier::new().justify_content(JustifyContent::CENTER)).child((
240 Box(Modifier::new()).child(with_content_color(
241 config.colors.title_content_color,
242 || title,
243 )),
244 subtitle
245 .map(|s| {
246 Box(Modifier::new()).child(with_content_color(
247 config.colors.subtitle_content_color,
248 || s,
249 ))
250 })
251 .unwrap_or(Box(Modifier::new())),
252 )),
253 ),
254 Row(Modifier::new()
255 .align_items(AlignItems::CENTER)
256 .clip_rounded(20.0))
257 .child(
258 actions
259 .into_iter()
260 .map(|a| {
261 with_content_color(config.colors.action_icon_content_color, move || a.clone())
262 })
263 .collect::<Vec<_>>(),
264 ),
265 ))
266}
267
268pub fn TopAppBar(
271 title: View,
272 subtitle: Option<View>,
273 navigation_icon: Option<View>,
274 actions: Vec<View>,
275 config: TopAppBarConfig,
276) -> View {
277 top_app_bar_layout(title, subtitle, navigation_icon, actions, config, false)
278}
279
280pub fn CenterAlignedTopAppBar(
282 title: View,
283 subtitle: Option<View>,
284 navigation_icon: Option<View>,
285 actions: Vec<View>,
286 config: TopAppBarConfig,
287) -> View {
288 top_app_bar_layout(title, subtitle, navigation_icon, actions, config, true)
289}
290
291#[derive(Clone, Debug)]
293pub struct SurfaceConfig {
294 pub modifier: Modifier,
295 pub enabled: bool,
296 pub color: Color,
297 pub content_color: Color,
298 pub shape_radius: f32,
299 pub tonal_elevation: f32,
300 pub shadow_elevation: f32,
301 pub border: Option<(f32, Color)>,
302 pub interaction_source: Option<MutableInteractionSource>,
303}
304
305impl Default for SurfaceConfig {
306 fn default() -> Self {
307 Self {
308 modifier: Modifier::new(),
309 enabled: true,
310 color: SurfaceDefaults::color(),
311 content_color: SurfaceDefaults::content_color(),
312 shape_radius: SurfaceDefaults::SHAPE_RADIUS,
313 tonal_elevation: SurfaceDefaults::TONAL_ELEVATION,
314 shadow_elevation: SurfaceDefaults::SHADOW_ELEVATION,
315 border: None,
316 interaction_source: None,
317 }
318 }
319}
320
321pub fn Surface(config: SurfaceConfig, content: impl FnOnce() -> View) -> View {
324 let sf_source: Rc<MutableInteractionSource> = config
325 .interaction_source
326 .clone()
327 .map(Rc::new)
328 .unwrap_or_else(|| remember(MutableInteractionSource::new));
329 let mut m = Modifier::new()
330 .background(config.color)
331 .clip_rounded(config.shape_radius)
332 .interaction_source(&*sf_source)
333 .then(config.modifier);
334 if config.tonal_elevation > 0.0 {
335 m = m.state_elevation(StateElevation {
336 default: config.tonal_elevation,
337 hovered: config.tonal_elevation,
338 pressed: config.tonal_elevation,
339 disabled: 0.0,
340 });
341 }
342 if config.shadow_elevation > 0.0 {
343 m = m.shadow(config.shadow_elevation, 0.0);
344 }
345 if let Some((w, c)) = config.border {
346 m = m.border(w, c, config.shape_radius);
347 }
348 Box(m).color(config.content_color).child(content())
349}
350
351#[derive(Clone, Copy, Debug)]
353pub struct IconButtonColors {
354 pub container_color: Color,
355 pub content_color: Color,
356 pub disabled_container_color: Color,
357 pub disabled_content_color: Color,
358}
359
360impl IconButtonColors {
361 pub fn container(&self, enabled: bool) -> Color {
362 if enabled {
363 self.container_color
364 } else {
365 self.disabled_container_color
366 }
367 }
368 pub fn content(&self, enabled: bool) -> Color {
369 if enabled {
370 self.content_color
371 } else {
372 self.disabled_content_color
373 }
374 }
375}
376
377#[derive(Clone, Debug)]
379pub struct IconButtonConfig {
380 pub modifier: Modifier,
381 pub enabled: bool,
382 pub colors: IconButtonColors,
383 pub container_size: Option<f32>,
384 pub interaction_source: Option<MutableInteractionSource>,
385 pub shape_radius: Option<f32>,
386}
387
388impl Default for IconButtonConfig {
389 fn default() -> Self {
390 Self {
391 modifier: Modifier::new(),
392 enabled: true,
393 colors: IconButtonColors {
394 container_color: Color::TRANSPARENT,
395 content_color: IconButtonDefaults::content_color(),
396 disabled_container_color: Color::TRANSPARENT,
397 disabled_content_color: IconButtonDefaults::disabled_content_color(),
398 },
399 container_size: None,
400 interaction_source: None,
401 shape_radius: None,
402 }
403 }
404}
405
406fn icon_button_render(
407 icon: View,
408 on_click: impl Fn() + 'static,
409 config: &IconButtonConfig,
410 sz: f32,
411 bg: Option<Color>,
412 bdr: Option<(f32, Color)>,
413 state_colors: StateColors,
414) -> View {
415 let is_enabled = config.enabled;
416 let content_color = config.colors.content(is_enabled);
417 let radius = config.shape_radius.unwrap_or(sz * 0.5);
418 let mut m = Modifier::new()
419 .size(sz, sz)
420 .clip_rounded(radius)
421 .state_colors(state_colors)
422 .align_items(AlignItems::CENTER)
423 .justify_content(JustifyContent::CENTER)
424 .then(config.modifier.clone());
425
426 if let Some(bg_color) = bg {
427 m = m.background(bg_color);
428 }
429 if let Some((w, c)) = bdr {
430 m = m.border(w, c, radius);
431 }
432 let source: Rc<MutableInteractionSource> = config
433 .interaction_source
434 .clone()
435 .map(Rc::new)
436 .unwrap_or_else(|| remember(MutableInteractionSource::new));
437 m = m.interaction_source(&*source);
438 if is_enabled {
439 m = m.clickable().on_click(move || on_click());
440 }
441
442 Box(m).child(icon)
443}
444
445pub fn IconButton(icon: View, on_click: impl Fn() + 'static, config: IconButtonConfig) -> View {
447 let th = theme();
448 let sz = config
449 .container_size
450 .unwrap_or(IconButtonDefaults::CONTAINER_SIZE);
451 icon_button_render(
452 icon,
453 on_click,
454 &config,
455 sz,
456 None,
457 None,
458 StateColors {
459 default: Color::TRANSPARENT,
460 hovered: th.on_surface.with_alpha_f32(0.08),
461 pressed: th.on_surface.with_alpha_f32(0.12),
462 disabled: Color::TRANSPARENT,
463 },
464 )
465}
466
467pub fn FilledIconButton(
469 icon: View,
470 on_click: impl Fn() + 'static,
471 config: IconButtonConfig,
472) -> View {
473 let th = theme();
474 let is_enabled = config.enabled;
475 let sz = config
476 .container_size
477 .unwrap_or(IconButtonDefaults::FILLED_CONTAINER_SIZE);
478 let bg = config.colors.container(is_enabled);
479 let content_color = config.colors.content(is_enabled);
480 icon_button_render(
481 icon,
482 on_click,
483 &config,
484 sz,
485 Some(bg),
486 None,
487 StateColors {
488 default: Color::TRANSPARENT,
489 hovered: content_color.with_alpha_f32(0.08),
490 pressed: content_color.with_alpha_f32(0.12),
491 disabled: th.on_surface.with_alpha_f32(0.12),
492 },
493 )
494}
495
496pub fn FilledTonalIconButton(
498 icon: View,
499 on_click: impl Fn() + 'static,
500 config: IconButtonConfig,
501) -> View {
502 let th = theme();
503 let is_enabled = config.enabled;
504 let sz = config
505 .container_size
506 .unwrap_or(IconButtonDefaults::FILLED_CONTAINER_SIZE);
507 let bg = config.colors.container(is_enabled);
508 let content_color = config.colors.content(is_enabled);
509 icon_button_render(
510 icon,
511 on_click,
512 &config,
513 sz,
514 Some(bg),
515 None,
516 StateColors {
517 default: Color::TRANSPARENT,
518 hovered: content_color.with_alpha_f32(0.08),
519 pressed: content_color.with_alpha_f32(0.12),
520 disabled: th.on_surface.with_alpha_f32(0.12),
521 },
522 )
523}
524
525pub fn OutlinedIconButton(
527 icon: View,
528 on_click: impl Fn() + 'static,
529 config: IconButtonConfig,
530) -> View {
531 let th = theme();
532 let sz = config
533 .container_size
534 .unwrap_or(IconButtonDefaults::CONTAINER_SIZE);
535 let border_color = if config.enabled {
536 th.outline
537 } else {
538 th.on_surface.with_alpha_f32(0.12)
539 };
540 icon_button_render(
541 icon,
542 on_click,
543 &config,
544 sz,
545 None,
546 Some((1.0, border_color)),
547 StateColors {
548 default: Color::TRANSPARENT,
549 hovered: th.on_surface.with_alpha_f32(0.08),
550 pressed: th.on_surface.with_alpha_f32(0.12),
551 disabled: Color::TRANSPARENT,
552 },
553 )
554}
555
556#[derive(Clone, Copy, Debug)]
558pub struct ButtonColors {
559 pub container_color: Color,
560 pub content_color: Color,
561 pub disabled_container_color: Color,
562 pub disabled_content_color: Color,
563}
564
565impl ButtonColors {
566 pub fn container(&self, enabled: bool) -> Color {
567 if enabled {
568 self.container_color
569 } else {
570 self.disabled_container_color
571 }
572 }
573 pub fn content(&self, enabled: bool) -> Color {
574 if enabled {
575 self.content_color
576 } else {
577 self.disabled_content_color
578 }
579 }
580}
581
582#[derive(Clone, Copy, Debug)]
584pub struct ButtonElevation {
585 pub default: f32,
586 pub pressed: f32,
587 pub focused: f32,
588 pub hovered: f32,
589 pub disabled: f32,
590}
591
592#[derive(Clone, Debug)]
594pub struct ButtonConfig {
595 pub modifier: Modifier,
596 pub enabled: bool,
597 pub content_color: Option<Color>,
598 pub container_color: Option<Color>,
599 pub state_colors: StateColors,
600 pub state_elevation: Option<StateElevation>,
601 pub border: Option<(f32, Color, f32)>,
602 pub shape_radius: f32,
603 pub content_padding: Option<PaddingValues>,
604 pub height: f32,
605 pub colors: Option<ButtonColors>,
606 pub elevation: Option<ButtonElevation>,
607 pub interaction_source: Option<MutableInteractionSource>,
608}
609
610impl Default for ButtonConfig {
611 fn default() -> Self {
612 Self {
613 modifier: Modifier::new(),
614 enabled: true,
615 content_color: None,
616 container_color: None,
617 state_colors: ButtonDefaults::state_colors_default(),
618 state_elevation: None,
619 border: None,
620 shape_radius: ButtonDefaults::SHAPE_RADIUS,
621 content_padding: None,
622 height: ButtonDefaults::HEIGHT,
623 colors: None,
624 elevation: None,
625 interaction_source: None,
626 }
627 }
628}
629
630fn resolve_button_colors(
633 config: &ButtonConfig,
634 def: ButtonColors,
635) -> (Color, Option<Color>, StateColors, Option<StateElevation>) {
636 if let Some(colors) = &config.colors {
637 let bg = if config.enabled {
638 colors.container_color
639 } else {
640 colors.disabled_container_color
641 };
642 let cc = if config.enabled {
643 colors.content_color
644 } else {
645 colors.disabled_content_color
646 };
647 let sc = StateColors {
648 default: Color::TRANSPARENT,
649 hovered: colors.content_color.with_alpha_f32(0.08),
650 pressed: colors.content_color.with_alpha_f32(0.12),
651 disabled: Color::TRANSPARENT,
652 };
653 let se = config.elevation.map(|e| StateElevation {
654 default: e.default,
655 hovered: e.hovered,
656 pressed: e.pressed,
657 disabled: e.disabled,
658 });
659 (cc, Some(bg), sc, se)
660 } else {
661 let cc = config.content_color.unwrap_or(def.content_color);
662 let bg = Some(config.container_color.unwrap_or(def.container_color));
663 let sc = if config.enabled {
664 config.state_colors
665 } else {
666 StateColors {
667 default: Color::TRANSPARENT,
668 hovered: Color::TRANSPARENT,
669 pressed: Color::TRANSPARENT,
670 disabled: config.state_colors.disabled,
671 }
672 };
673 let se = config.state_elevation;
674 (cc, bg, sc, se)
675 }
676}
677
678fn button_impl(
679 outer_modifier: Modifier,
680 on_click: impl Fn() + 'static,
681 content: impl FnOnce() -> View,
682 content_color: Color,
683 container_color: Option<Color>,
684 state_colors: StateColors,
685 state_elevation: Option<StateElevation>,
686 border: Option<(f32, Color, f32)>,
687 padding_left: f32,
688 padding_right: f32,
689 height: f32,
690 shape_radius: f32,
691 enabled: bool,
692 interaction_source: Option<MutableInteractionSource>,
693) -> View {
694 let mut m = Modifier::new().min_height(height).min_width(48.0).flex_shrink(0.0);
695 if let Some(bg) = container_color {
696 m = m.background(bg);
697 }
698 m = m.state_colors(if enabled {
699 state_colors
700 } else {
701 StateColors {
702 default: Color::TRANSPARENT,
703 hovered: Color::TRANSPARENT,
704 pressed: Color::TRANSPARENT,
705 disabled: state_colors.disabled,
706 }
707 });
708 if let Some(se) = state_elevation {
709 m = m.state_elevation(se);
710 }
711 if let Some((w, c, r)) = border {
712 m = m.border(w, c, r);
713 }
714 m = m
715 .clip_rounded(shape_radius)
716 .padding_values(PaddingValues {
717 left: padding_left,
718 right: padding_right,
719 top: 8.0,
720 bottom: 8.0,
721 })
722 .align_items(AlignItems::CENTER)
723 .justify_content(JustifyContent::CENTER);
724
725 let source: Rc<MutableInteractionSource> = interaction_source.map(Rc::new).unwrap_or_else(|| {
727 match outer_modifier.key {
728 Some(k) => {
729 remember_with_key(format!("m3_btn_src:{k}"), MutableInteractionSource::new)
730 }
731 None => remember(MutableInteractionSource::new),
732 }
733 });
734 m = m.interaction_source(&*source);
735 m = m.indication(ripple(RippleConfig {
736 color: Some(content_color),
737 bounded: true,
738 ..Default::default()
739 }));
740
741 if enabled {
742 m = m.clickable().on_click(move || on_click());
743 }
744 m = m.then(outer_modifier);
745 let effective = if enabled {
746 content_color
747 } else {
748 content_color.with_alpha_f32(0.38)
749 };
750 let content = with_content_color(effective, content);
751 Box(m).child(content)
752}
753
754pub fn Button(
757 modifier: Modifier,
758 on_click: impl Fn() + 'static,
759 config: ButtonConfig,
760 content: impl FnOnce() -> View,
761) -> View {
762 let def = ButtonColors {
763 container_color: ButtonDefaults::container_color(),
764 content_color: ButtonDefaults::content_color(),
765 disabled_container_color: ButtonDefaults::container_color()
766 .with_alpha_f32(0.12)
767 .composite_over(theme().surface_container_low),
768 disabled_content_color: ButtonDefaults::content_color().with_alpha_f32(0.38),
769 };
770 let (cc, bg, sc, se) = resolve_button_colors(&config, def);
771 let pad = config.content_padding.unwrap_or(PaddingValues {
772 left: 24.0,
773 right: 24.0,
774 top: 0.0,
775 bottom: 0.0,
776 });
777 button_impl(
778 modifier.then(config.modifier),
779 on_click,
780 content,
781 cc,
782 bg,
783 sc,
784 se.or(Some(ButtonDefaults::state_elevation_default())),
785 config.border,
786 pad.left,
787 pad.right,
788 config.height,
789 config.shape_radius,
790 config.enabled,
791 config.interaction_source.clone(),
792 )
793}
794
795pub fn FilledTonalButton(
797 modifier: Modifier,
798 on_click: impl Fn() + 'static,
799 config: ButtonConfig,
800 content: impl FnOnce() -> View,
801) -> View {
802 let th = theme();
803 let def = ButtonColors {
804 container_color: ButtonDefaults::tonal_container_color(),
805 content_color: ButtonDefaults::tonal_content_color(),
806 disabled_container_color: th
807 .on_surface
808 .with_alpha_f32(0.12)
809 .composite_over(th.surface_container_low),
810 disabled_content_color: th.on_surface.with_alpha_f32(0.38),
811 };
812 let (cc, bg, sc, se) = resolve_button_colors(&config, def);
813 let pad = config.content_padding.unwrap_or(PaddingValues {
814 left: 24.0,
815 right: 24.0,
816 top: 0.0,
817 bottom: 0.0,
818 });
819 button_impl(
820 modifier.then(config.modifier),
821 on_click,
822 content,
823 cc,
824 bg,
825 sc,
826 se.or(Some(ButtonDefaults::state_elevation_default())),
827 config.border,
828 pad.left,
829 pad.right,
830 config.height,
831 config.shape_radius,
832 config.enabled,
833 config.interaction_source.clone(),
834 )
835}
836
837pub fn OutlinedButton(
839 modifier: Modifier,
840 on_click: impl Fn() + 'static,
841 config: ButtonConfig,
842 content: impl FnOnce() -> View,
843) -> View {
844 let th = theme();
845 let def = ButtonColors {
846 container_color: Color::TRANSPARENT,
847 content_color: ButtonDefaults::outlined_content_color(),
848 disabled_container_color: Color::TRANSPARENT,
849 disabled_content_color: th.on_surface.with_alpha_f32(0.38),
850 };
851 let (cc, bg, sc, se) = resolve_button_colors(&config, def);
852 let border = config
853 .border
854 .unwrap_or((1.0, ButtonDefaults::outlined_border_color(), 20.0));
855 let pad = config.content_padding.unwrap_or(PaddingValues {
856 left: 24.0,
857 right: 24.0,
858 top: 0.0,
859 bottom: 0.0,
860 });
861 button_impl(
862 modifier.then(config.modifier),
863 on_click,
864 content,
865 cc,
866 bg,
867 sc,
868 se,
869 Some(border),
870 pad.left,
871 pad.right,
872 config.height,
873 config.shape_radius,
874 config.enabled,
875 config.interaction_source.clone(),
876 )
877}
878
879pub fn TextButton(
881 modifier: Modifier,
882 on_click: impl Fn() + 'static,
883 config: ButtonConfig,
884 content: impl FnOnce() -> View,
885) -> View {
886 let th = theme();
887 let def = ButtonColors {
888 container_color: Color::TRANSPARENT,
889 content_color: ButtonDefaults::text_content_color(),
890 disabled_container_color: Color::TRANSPARENT,
891 disabled_content_color: th.on_surface.with_alpha_f32(0.38),
892 };
893 let (cc, bg, sc, se) = resolve_button_colors(&config, def);
894 let pad = config.content_padding.unwrap_or(PaddingValues {
895 left: 12.0,
896 right: 12.0,
897 top: 0.0,
898 bottom: 0.0,
899 });
900 button_impl(
901 modifier.then(config.modifier),
902 on_click,
903 content,
904 cc,
905 bg,
906 sc,
907 se,
908 None,
909 pad.left,
910 pad.right,
911 config.height,
912 config.shape_radius,
913 config.enabled,
914 config.interaction_source.clone(),
915 )
916}
917
918pub fn ElevatedButton(
920 modifier: Modifier,
921 on_click: impl Fn() + 'static,
922 config: ButtonConfig,
923 content: impl FnOnce() -> View,
924) -> View {
925 let th = theme();
926 let def = ButtonColors {
927 container_color: ButtonDefaults::elevated_container_color(),
928 content_color: ButtonDefaults::elevated_content_color(),
929 disabled_container_color: th.on_surface.with_alpha_f32(0.04),
930 disabled_content_color: th.on_surface.with_alpha_f32(0.38),
931 };
932 let (cc, bg, sc, se) = resolve_button_colors(&config, def);
933 let pad = config.content_padding.unwrap_or(PaddingValues {
934 left: 24.0,
935 right: 24.0,
936 top: 0.0,
937 bottom: 0.0,
938 });
939 button_impl(
940 modifier.then(config.modifier),
941 on_click,
942 content,
943 cc,
944 bg,
945 sc,
946 se.or(Some(ButtonDefaults::elevated_state_elevation())),
947 config.border,
948 pad.left,
949 pad.right,
950 config.height,
951 config.shape_radius,
952 config.enabled,
953 config.interaction_source.clone(),
954 )
955}
956
957#[derive(Clone, Debug)]
959pub struct ToggleButtonConfig {
960 pub modifier: Modifier,
961 pub enabled: bool,
962 pub container_color: Option<Color>,
963 pub content_color: Option<Color>,
964 pub checked_container_color: Option<Color>,
965 pub checked_content_color: Option<Color>,
966 pub state_colors: StateColors,
967 pub state_elevation: Option<StateElevation>,
968 pub border: Option<(f32, Color, f32)>,
969 pub shape_radius: f32,
970 pub height: f32,
971 pub content_padding: Option<PaddingValues>,
972 pub interaction_source: Option<MutableInteractionSource>,
973}
974
975impl Default for ToggleButtonConfig {
976 fn default() -> Self {
977 Self {
978 modifier: Modifier::new(),
979 enabled: true,
980 container_color: None,
981 content_color: None,
982 checked_container_color: None,
983 checked_content_color: None,
984 state_colors: ToggleButtonDefaults::state_colors_default(),
985 state_elevation: None,
986 border: None,
987 shape_radius: ToggleButtonDefaults::SHAPE_RADIUS,
988 height: ToggleButtonDefaults::HEIGHT,
989 content_padding: None,
990 interaction_source: None,
991 }
992 }
993}
994
995fn toggle_button_impl(
996 checked: bool,
997 on_checked_change: impl Fn(bool) + 'static,
998 content: impl FnOnce(bool) -> View,
999 content_color: Color,
1000 container_color: Option<Color>,
1001 checked_container_color: Option<Color>,
1002 checked_content_color: Option<Color>,
1003 state_colors: StateColors,
1004 state_elevation: StateElevation,
1005 border: Option<(f32, Color, f32)>,
1006 pad_left: f32,
1007 pad_right: f32,
1008 height: f32,
1009 shape_radius: f32,
1010 enabled: bool,
1011 interaction_source: Option<MutableInteractionSource>,
1012) -> View {
1013 let th = theme();
1014 let bg = if checked {
1015 checked_container_color.unwrap_or(th.primary)
1016 } else {
1017 container_color.unwrap_or(Color::TRANSPARENT)
1018 };
1019 let fg = if checked {
1020 checked_content_color.unwrap_or(th.on_primary)
1021 } else {
1022 content_color
1023 };
1024 let mut m = Modifier::new()
1025 .min_height(height)
1026 .padding_values(PaddingValues {
1027 left: pad_left,
1028 right: pad_right,
1029 top: 8.0,
1030 bottom: 8.0,
1031 })
1032 .background(bg)
1033 .clip_rounded(shape_radius)
1034 .align_items(AlignItems::CENTER)
1035 .justify_content(JustifyContent::CENTER)
1036 .state_colors(state_colors)
1037 .state_elevation(state_elevation);
1038 let tg_source: Rc<MutableInteractionSource> = interaction_source
1039 .map(Rc::new)
1040 .unwrap_or_else(|| remember(MutableInteractionSource::new));
1041 m = m.interaction_source(&*tg_source);
1042 if let Some((w, c, r)) = border {
1043 m = m.border(w, c, r);
1044 }
1045 if enabled {
1046 let cb = on_checked_change;
1047 m = m.clickable().on_click(move || cb(!checked));
1048 } else {
1049 m = m.alpha(0.38);
1050 }
1051 with_content_color(fg, || Box(m).child(content(checked)))
1052}
1053
1054pub fn ToggleButton(
1056 checked: bool,
1057 on_checked_change: impl Fn(bool) + 'static,
1058 config: ToggleButtonConfig,
1059 content: impl FnOnce(bool) -> View,
1060) -> View {
1061 let cc = config
1062 .content_color
1063 .unwrap_or_else(ToggleButtonDefaults::content_color);
1064 let checked_cc = config
1065 .checked_content_color
1066 .unwrap_or_else(ToggleButtonDefaults::checked_content_color);
1067 let checked_bg = config
1068 .checked_container_color
1069 .unwrap_or_else(ToggleButtonDefaults::checked_container_color);
1070 let se = config
1071 .state_elevation
1072 .unwrap_or_else(ToggleButtonDefaults::state_elevation_default);
1073 let pad_l = config
1074 .content_padding
1075 .map(|p| p.left)
1076 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING);
1077 let pad_r = config
1078 .content_padding
1079 .map(|p| p.right)
1080 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING);
1081 toggle_button_impl(
1082 checked,
1083 on_checked_change,
1084 content,
1085 cc,
1086 None,
1087 Some(checked_bg),
1088 Some(checked_cc),
1089 config.state_colors,
1090 se,
1091 config.border,
1092 pad_l,
1093 pad_r,
1094 config.height,
1095 config.shape_radius,
1096 config.enabled,
1097 config.interaction_source.clone(),
1098 )
1099}
1100
1101pub fn TonalToggleButton(
1103 checked: bool,
1104 on_checked_change: impl Fn(bool) + 'static,
1105 config: ToggleButtonConfig,
1106 content: impl FnOnce(bool) -> View,
1107) -> View {
1108 let cc = config
1109 .content_color
1110 .unwrap_or_else(ToggleButtonDefaults::tonal_content_color);
1111 let checked_cc = config
1112 .checked_content_color
1113 .unwrap_or_else(ToggleButtonDefaults::tonal_checked_content_color);
1114 let checked_bg = config
1115 .checked_container_color
1116 .unwrap_or_else(ToggleButtonDefaults::tonal_checked_container_color);
1117 let se = config
1118 .state_elevation
1119 .unwrap_or_else(ToggleButtonDefaults::state_elevation_default);
1120 toggle_button_impl(
1121 checked,
1122 on_checked_change,
1123 content,
1124 cc,
1125 None,
1126 Some(checked_bg),
1127 Some(checked_cc),
1128 config.state_colors,
1129 se,
1130 config.border,
1131 config
1132 .content_padding
1133 .map(|p| p.left)
1134 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING),
1135 config
1136 .content_padding
1137 .map(|p| p.right)
1138 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING),
1139 config.height,
1140 config.shape_radius,
1141 config.enabled,
1142 config.interaction_source.clone(),
1143 )
1144}
1145
1146pub fn OutlinedToggleButton(
1148 checked: bool,
1149 on_checked_change: impl Fn(bool) + 'static,
1150 config: ToggleButtonConfig,
1151 content: impl FnOnce(bool) -> View,
1152) -> View {
1153 let cc = config
1154 .content_color
1155 .unwrap_or_else(ToggleButtonDefaults::outlined_content_color);
1156 let checked_cc = config
1157 .checked_content_color
1158 .unwrap_or_else(ToggleButtonDefaults::outlined_checked_content_color);
1159 let checked_bg = config
1160 .checked_container_color
1161 .unwrap_or_else(ToggleButtonDefaults::outlined_checked_container_color);
1162 let se = config
1163 .state_elevation
1164 .unwrap_or_else(ToggleButtonDefaults::state_elevation_default);
1165 let border = if !checked {
1166 Some(config.border.unwrap_or((
1167 1.0,
1168 ToggleButtonDefaults::outlined_border_color(),
1169 config.shape_radius,
1170 )))
1171 } else {
1172 config.border
1173 };
1174 toggle_button_impl(
1175 checked,
1176 on_checked_change,
1177 content,
1178 cc,
1179 None,
1180 Some(checked_bg),
1181 Some(checked_cc),
1182 config.state_colors,
1183 se,
1184 border,
1185 config
1186 .content_padding
1187 .map(|p| p.left)
1188 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING),
1189 config
1190 .content_padding
1191 .map(|p| p.right)
1192 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING),
1193 config.height,
1194 config.shape_radius,
1195 config.enabled,
1196 config.interaction_source.clone(),
1197 )
1198}
1199
1200pub fn ElevatedToggleButton(
1202 checked: bool,
1203 on_checked_change: impl Fn(bool) + 'static,
1204 config: ToggleButtonConfig,
1205 content: impl FnOnce(bool) -> View,
1206) -> View {
1207 let cc = config
1208 .content_color
1209 .unwrap_or_else(ToggleButtonDefaults::elevated_content_color);
1210 let checked_cc = config
1211 .checked_content_color
1212 .unwrap_or_else(ToggleButtonDefaults::elevated_checked_content_color);
1213 let checked_bg = config
1214 .checked_container_color
1215 .unwrap_or_else(ToggleButtonDefaults::elevated_checked_container_color);
1216 let se = config
1217 .state_elevation
1218 .unwrap_or_else(ToggleButtonDefaults::elevated_state_elevation);
1219 toggle_button_impl(
1220 checked,
1221 on_checked_change,
1222 content,
1223 cc,
1224 None,
1225 Some(checked_bg),
1226 Some(checked_cc),
1227 config.state_colors,
1228 se,
1229 config.border,
1230 config
1231 .content_padding
1232 .map(|p| p.left)
1233 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING),
1234 config
1235 .content_padding
1236 .map(|p| p.right)
1237 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING),
1238 config.height,
1239 config.shape_radius,
1240 config.enabled,
1241 config.interaction_source.clone(),
1242 )
1243}
1244
1245#[derive(Clone, Debug)]
1247pub struct FABConfig {
1248 pub modifier: Modifier,
1249 pub enabled: bool,
1250 pub container_color: Color,
1251 pub content_color: Color,
1252 pub state_elevation: StateElevation,
1253 pub shape_radius: f32,
1254 pub size: f32,
1255 pub interaction_source: Option<MutableInteractionSource>,
1256}
1257
1258impl Default for FABConfig {
1259 fn default() -> Self {
1260 Self {
1261 modifier: Modifier::new(),
1262 enabled: true,
1263 container_color: FABDefaults::container_color(),
1264 content_color: FABDefaults::content_color(),
1265 state_elevation: FABDefaults::state_elevation(),
1266 shape_radius: FABDefaults::SHAPE_RADIUS,
1267 size: FABDefaults::SIZE,
1268 interaction_source: None,
1269 }
1270 }
1271}
1272
1273fn fab_impl(
1274 icon: View,
1275 on_click: impl Fn() + 'static,
1276 size: f32,
1277 shape_r: f32,
1278 config: FABConfig,
1279) -> View {
1280 let th = theme();
1281 let is_enabled = config.enabled;
1282 let bg = if is_enabled {
1283 config.container_color
1284 } else {
1285 th.on_surface
1286 .with_alpha_f32(0.12)
1287 .composite_over(th.surface_container_low)
1288 };
1289 let content_color = if is_enabled {
1290 config.content_color
1291 } else {
1292 th.on_surface.with_alpha_f32(0.38)
1293 };
1294
1295 let mut m = Modifier::new()
1296 .size(size, size)
1297 .background(bg)
1298 .state_colors(StateColors {
1299 default: Color::TRANSPARENT,
1300 hovered: config.content_color.with_alpha_f32(0.08),
1301 pressed: config.content_color.with_alpha_f32(0.12),
1302 disabled: th.on_surface.with_alpha_f32(0.12),
1303 })
1304 .state_elevation(config.state_elevation)
1305 .clip_rounded(shape_r)
1306 .align_items(AlignItems::CENTER)
1307 .justify_content(JustifyContent::CENTER)
1308 .then(config.modifier);
1309
1310 let source: Rc<MutableInteractionSource> = config
1311 .interaction_source
1312 .map(Rc::new)
1313 .unwrap_or_else(|| remember(MutableInteractionSource::new));
1314 m = m.interaction_source(&*source);
1315 if is_enabled {
1316 m = m.clickable().on_click(move || on_click());
1317 }
1318
1319 Box(m).child(icon)
1320}
1321
1322pub fn FAB(icon: View, on_click: impl Fn() + 'static, config: FABConfig) -> View {
1324 fab_impl(
1325 icon,
1326 on_click,
1327 FABDefaults::SIZE,
1328 FABDefaults::SHAPE_RADIUS,
1329 config,
1330 )
1331}
1332
1333pub fn SmallFAB(icon: View, on_click: impl Fn() + 'static, config: FABConfig) -> View {
1335 fab_impl(
1336 icon,
1337 on_click,
1338 FABDefaults::SMALL_SIZE,
1339 FABDefaults::SMALL_SHAPE_RADIUS,
1340 config,
1341 )
1342}
1343
1344pub fn LargeFAB(icon: View, on_click: impl Fn() + 'static, config: FABConfig) -> View {
1346 fab_impl(
1347 icon,
1348 on_click,
1349 FABDefaults::LARGE_SIZE,
1350 FABDefaults::LARGE_SHAPE_RADIUS,
1351 config,
1352 )
1353}
1354
1355pub fn ExtendedFAB(
1357 icon: Option<View>,
1358 label: impl Into<String>,
1359 on_click: impl Fn() + 'static,
1360 config: FABConfig,
1361) -> View {
1362 let th = theme();
1363 let has_icon = icon.is_some();
1364 let is_enabled = config.enabled;
1365 let bg = if is_enabled {
1366 config.container_color
1367 } else {
1368 th.on_surface
1369 .with_alpha_f32(0.12)
1370 .composite_over(th.surface_container_low)
1371 };
1372 let content_color = if is_enabled {
1373 config.content_color
1374 } else {
1375 th.on_surface.with_alpha_f32(0.38)
1376 };
1377
1378 let mut m = Modifier::new()
1379 .height(56.0)
1380 .min_width(80.0)
1381 .background(bg)
1382 .state_colors(StateColors {
1383 default: Color::TRANSPARENT,
1384 hovered: config.content_color.with_alpha_f32(0.08),
1385 pressed: config.content_color.with_alpha_f32(0.12),
1386 disabled: theme().on_surface.with_alpha_f32(0.12),
1387 })
1388 .state_elevation(config.state_elevation)
1389 .clip_rounded(FABDefaults::SHAPE_RADIUS)
1390 .padding_values(PaddingValues {
1391 left: 16.0,
1392 right: 20.0,
1393 top: 0.0,
1394 bottom: 0.0,
1395 })
1396 .align_items(AlignItems::CENTER);
1397
1398 if is_enabled {
1399 m = m.clickable().on_click(move || on_click());
1400 }
1401 m = m.then(config.modifier);
1402 Row(m).child((
1403 icon.unwrap_or(Box(Modifier::new())),
1404 Box(Modifier::new()
1405 .width(if has_icon { 12.0 } else { 0.0 })
1406 .fill_max_height()),
1407 Text(label)
1408 .color(content_color)
1409 .size(th.typography.label_large)
1410 .single_line(),
1411 ))
1412}
1413
1414#[derive(Clone, Debug)]
1416pub struct DividerConfig {
1417 pub modifier: Modifier,
1418 pub thickness: f32,
1419 pub color: Color,
1420}
1421
1422impl Default for DividerConfig {
1423 fn default() -> Self {
1424 Self {
1425 modifier: Modifier::new(),
1426 thickness: DividerDefaults::THICKNESS,
1427 color: DividerDefaults::color(),
1428 }
1429 }
1430}
1431
1432pub fn HorizontalDivider(config: DividerConfig) -> View {
1435 Box(Modifier::new()
1436 .min_width(200.0)
1437 .height(config.thickness)
1438 .background(config.color)
1439 .then(config.modifier))
1440}
1441
1442#[deprecated(since = "0.19.5", note = "renamed to HorizontalDivider")]
1443pub fn Divider(config: DividerConfig) -> View {
1444 HorizontalDivider(config)
1445}
1446
1447pub fn VerticalDivider(config: DividerConfig) -> View {
1449 Box(Modifier::new()
1450 .width(config.thickness)
1451 .fill_max_height()
1452 .background(config.color)
1453 .then(config.modifier))
1454}
1455
1456#[derive(Clone, Debug)]
1458pub struct BadgeConfig {
1459 pub modifier: Modifier,
1460 pub container_color: Color,
1461 pub content_color: Color,
1462}
1463
1464impl Default for BadgeConfig {
1465 fn default() -> Self {
1466 Self {
1467 modifier: Modifier::new(),
1468 container_color: BadgeDefaults::container_color(),
1469 content_color: BadgeDefaults::content_color(),
1470 }
1471 }
1472}
1473
1474pub fn Badge(content: Option<View>, config: BadgeConfig) -> View {
1477 match content {
1478 None => Box(Modifier::new()
1479 .size(BadgeDefaults::DOT_SIZE, BadgeDefaults::DOT_SIZE)
1480 .background(config.container_color)
1481 .clip_rounded(BadgeDefaults::DOT_SIZE * 0.5)
1482 .flex_shrink(0.0)
1483 .then(config.modifier)),
1484 Some(view) => Box(Modifier::new()
1485 .min_width(BadgeDefaults::LABEL_MIN_WIDTH)
1486 .height(BadgeDefaults::LABEL_HEIGHT)
1487 .background(config.container_color)
1488 .clip_rounded(BadgeDefaults::LABEL_HEIGHT * 0.5)
1489 .padding_values(PaddingValues {
1490 left: 4.0,
1491 right: 4.0,
1492 top: 0.0,
1493 bottom: 0.0,
1494 })
1495 .align_items(AlignItems::CENTER)
1496 .justify_content(JustifyContent::CENTER)
1497 .flex_shrink(0.0)
1498 .then(config.modifier))
1499 .child(with_content_color(config.content_color, move || view)),
1500 }
1501}
1502
1503#[derive(Clone, Debug)]
1505pub struct BadgedBoxConfig {
1506 pub modifier: Modifier,
1507 pub dot_offset_x: f32,
1509 pub dot_offset_y: f32,
1511 pub content_offset_x: f32,
1513 pub content_offset_y: f32,
1515 pub has_content: bool,
1517}
1518
1519impl Default for BadgedBoxConfig {
1520 fn default() -> Self {
1521 Self {
1522 modifier: Modifier::new(),
1523 dot_offset_x: BadgeDefaults::DOT_OFFSET_X,
1524 dot_offset_y: BadgeDefaults::DOT_OFFSET_Y,
1525 content_offset_x: BadgeDefaults::CONTENT_OFFSET_X,
1526 content_offset_y: BadgeDefaults::CONTENT_OFFSET_Y,
1527 has_content: false,
1528 }
1529 }
1530}
1531
1532pub fn BadgedBox(badge: View, content: View, config: BadgedBoxConfig) -> View {
1534 let (top, right) = if config.has_content {
1535 (
1536 config.content_offset_y - BadgeDefaults::LABEL_HEIGHT, config.content_offset_x - BadgeDefaults::LABEL_MIN_WIDTH, )
1539 } else {
1540 (
1541 config.dot_offset_y - BadgeDefaults::DOT_SIZE, config.dot_offset_x - BadgeDefaults::DOT_SIZE, )
1544 };
1545
1546 Box(config.modifier.flex_shrink(0.0)).child((
1547 content,
1548 Box(Modifier::new()
1549 .absolute()
1550 .offset(None, Some(top), Some(right), None)
1551 .flex_shrink(0.0)
1552 .hit_passthrough())
1553 .child(badge),
1554 ))
1555}
1556
1557#[derive(Clone, Debug)]
1560pub struct ListItemColors {
1561 pub container_color: Color,
1562 pub headline_color: Color,
1563 pub supporting_color: Color,
1564 pub overline_color: Color,
1565 pub leading_icon_color: Color,
1566 pub trailing_icon_color: Color,
1567
1568 pub disabled_container_color: Color,
1569 pub disabled_headline_color: Color,
1570 pub disabled_supporting_color: Color,
1571 pub disabled_overline_color: Color,
1572 pub disabled_leading_icon_color: Color,
1573 pub disabled_trailing_icon_color: Color,
1574
1575 pub selected_container_color: Color,
1576 pub selected_headline_color: Color,
1577 pub selected_supporting_color: Color,
1578 pub selected_overline_color: Color,
1579 pub selected_leading_icon_color: Color,
1580 pub selected_trailing_icon_color: Color,
1581
1582 pub dragged_container_color: Color,
1583 pub dragged_headline_color: Color,
1584 pub dragged_supporting_color: Color,
1585 pub dragged_overline_color: Color,
1586 pub dragged_leading_icon_color: Color,
1587 pub dragged_trailing_icon_color: Color,
1588}
1589
1590impl ListItemColors {
1591 pub fn container(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1592 if !enabled {
1593 self.disabled_container_color
1594 } else if dragged {
1595 self.dragged_container_color
1596 } else if selected {
1597 self.selected_container_color
1598 } else {
1599 self.container_color
1600 }
1601 }
1602 pub fn headline(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1603 if !enabled {
1604 self.disabled_headline_color
1605 } else if dragged {
1606 self.dragged_headline_color
1607 } else if selected {
1608 self.selected_headline_color
1609 } else {
1610 self.headline_color
1611 }
1612 }
1613 pub fn supporting(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1614 if !enabled {
1615 self.disabled_supporting_color
1616 } else if dragged {
1617 self.dragged_supporting_color
1618 } else if selected {
1619 self.selected_supporting_color
1620 } else {
1621 self.supporting_color
1622 }
1623 }
1624 pub fn overline(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1625 if !enabled {
1626 self.disabled_overline_color
1627 } else if dragged {
1628 self.dragged_overline_color
1629 } else if selected {
1630 self.selected_overline_color
1631 } else {
1632 self.overline_color
1633 }
1634 }
1635 pub fn leading_icon(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1636 if !enabled {
1637 self.disabled_leading_icon_color
1638 } else if dragged {
1639 self.dragged_leading_icon_color
1640 } else if selected {
1641 self.selected_leading_icon_color
1642 } else {
1643 self.leading_icon_color
1644 }
1645 }
1646 pub fn trailing_icon(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1647 if !enabled {
1648 self.disabled_trailing_icon_color
1649 } else if dragged {
1650 self.dragged_trailing_icon_color
1651 } else if selected {
1652 self.selected_trailing_icon_color
1653 } else {
1654 self.trailing_icon_color
1655 }
1656 }
1657}
1658
1659impl Default for ListItemColors {
1660 fn default() -> Self {
1661 Self {
1662 container_color: Color::TRANSPARENT,
1663 headline_color: ListItemDefaults::headline_color(),
1664 supporting_color: ListItemDefaults::supporting_color(),
1665 overline_color: ListItemDefaults::overline_color(),
1666 leading_icon_color: ListItemDefaults::leading_icon_color(),
1667 trailing_icon_color: ListItemDefaults::trailing_icon_color(),
1668 disabled_container_color: ListItemDefaults::disabled_container_color(),
1669 disabled_headline_color: ListItemDefaults::disabled_headline_color(),
1670 disabled_supporting_color: ListItemDefaults::disabled_supporting_color(),
1671 disabled_overline_color: ListItemDefaults::disabled_overline_color(),
1672 disabled_leading_icon_color: ListItemDefaults::disabled_leading_icon_color(),
1673 disabled_trailing_icon_color: ListItemDefaults::disabled_trailing_icon_color(),
1674 selected_container_color: ListItemDefaults::selected_container_color(),
1675 selected_headline_color: ListItemDefaults::selected_headline_color(),
1676 selected_supporting_color: ListItemDefaults::selected_supporting_color(),
1677 selected_overline_color: ListItemDefaults::selected_overline_color(),
1678 selected_leading_icon_color: ListItemDefaults::selected_leading_icon_color(),
1679 selected_trailing_icon_color: ListItemDefaults::selected_trailing_icon_color(),
1680 dragged_container_color: ListItemDefaults::dragged_container_color(),
1681 dragged_headline_color: ListItemDefaults::dragged_headline_color(),
1682 dragged_supporting_color: ListItemDefaults::dragged_supporting_color(),
1683 dragged_overline_color: ListItemDefaults::dragged_overline_color(),
1684 dragged_leading_icon_color: ListItemDefaults::dragged_leading_icon_color(),
1685 dragged_trailing_icon_color: ListItemDefaults::dragged_trailing_icon_color(),
1686 }
1687 }
1688}
1689
1690#[derive(Clone, Debug)]
1692pub struct ListItemConfig {
1693 pub modifier: Modifier,
1694 pub enabled: bool,
1696 pub selected: bool,
1697 pub colors: ListItemColors,
1698 pub state_colors: StateColors,
1699 pub tonal_elevation: f32,
1700 pub shadow_elevation: f32,
1701 pub shape_radius: f32,
1702 pub shape_radii: Option<[f32; 4]>,
1704 pub horizontal_padding: f32,
1705 pub trailing_padding: f32,
1706 pub one_line_height: f32,
1707 pub two_line_height: f32,
1708 pub three_line_height: f32,
1709 pub interaction_source: Option<MutableInteractionSource>,
1710}
1711
1712impl Default for ListItemConfig {
1713 fn default() -> Self {
1714 Self {
1715 modifier: Modifier::new(),
1716 enabled: true,
1717 selected: false,
1718 colors: ListItemColors::default(),
1719 state_colors: ListItemDefaults::state_colors_default(),
1720 tonal_elevation: 0.0,
1721 shadow_elevation: 0.0,
1722 shape_radius: 0.0,
1723 shape_radii: None,
1724 horizontal_padding: ListItemDefaults::HORIZONTAL_PADDING,
1725 trailing_padding: ListItemDefaults::TRAILING_PADDING,
1726 one_line_height: ListItemDefaults::ONE_LINE_HEIGHT,
1727 two_line_height: ListItemDefaults::TWO_LINE_HEIGHT,
1728 three_line_height: ListItemDefaults::THREE_LINE_HEIGHT,
1729 interaction_source: None,
1730 }
1731 }
1732}
1733
1734static LISTITEM_COUNTER: AtomicU64 = AtomicU64::new(0);
1735
1736pub fn ListItem(
1739 headline: impl Into<String>,
1740 supporting_text: Option<String>,
1741 overline_text: Option<String>,
1742 leading: Option<View>,
1743 trailing: Option<View>,
1744 on_click: Option<Rc<dyn Fn()>>,
1745 on_long_click: Option<Rc<dyn Fn()>>,
1746 config: ListItemConfig,
1747) -> View {
1748 let th = theme();
1749 let is_enabled = config.enabled;
1750 let is_selected = config.selected;
1751 let c = &config.colors;
1752 let id = remember(|| LISTITEM_COUNTER.fetch_add(1, Ordering::Relaxed));
1753 let spec = th.motion.color;
1754
1755 let hd_col = animate_color(
1756 format!("li_hd_{}", id),
1757 c.headline(is_enabled, is_selected, false),
1758 spec,
1759 );
1760 let sp_col = animate_color(
1761 format!("li_sp_{}", id),
1762 c.supporting(is_enabled, is_selected, false),
1763 spec,
1764 );
1765 let ol_col = animate_color(
1766 format!("li_ol_{}", id),
1767 c.overline(is_enabled, is_selected, false),
1768 spec,
1769 );
1770 let ld_col = animate_color(
1771 format!("li_ld_{}", id),
1772 c.leading_icon(is_enabled, is_selected, false),
1773 spec,
1774 );
1775 let tr_col = animate_color(
1776 format!("li_tr_{}", id),
1777 c.trailing_icon(is_enabled, is_selected, false),
1778 spec,
1779 );
1780 let bg = animate_color(
1781 format!("li_bg_{}", id),
1782 c.container(is_enabled, is_selected, false),
1783 spec,
1784 );
1785
1786 let line_count = match (overline_text.is_some(), supporting_text.is_some()) {
1787 (true, true) => 3,
1788 (true, false) | (false, true) => 2,
1789 (false, false) => 1,
1790 };
1791 let min_h = match line_count {
1792 3 => config.three_line_height,
1793 2 => config.two_line_height,
1794 _ => config.one_line_height,
1795 };
1796 let top_bottom_padding = match line_count {
1797 3 => 12.0,
1798 _ => 8.0,
1799 };
1800
1801 let vert_align = if min_h >= config.three_line_height {
1802 AlignItems::START
1803 } else {
1804 AlignItems::CENTER
1805 };
1806
1807 let li_source: Rc<MutableInteractionSource> = config
1808 .interaction_source
1809 .clone()
1810 .map(Rc::new)
1811 .unwrap_or_else(|| remember(MutableInteractionSource::new));
1812
1813 let mut modifier = Modifier::new()
1814 .min_width(200.0)
1815 .min_height(min_h)
1816 .background(bg);
1817 match config.shape_radii {
1818 Some(r) => modifier = modifier.clip_rounded_radii(r),
1819 None => modifier = modifier.clip_rounded(config.shape_radius),
1820 }
1821 modifier = modifier
1822 .state_colors(config.state_colors)
1823 .padding_values(PaddingValues {
1824 left: config.horizontal_padding,
1825 right: config.trailing_padding,
1826 top: top_bottom_padding,
1827 bottom: top_bottom_padding,
1828 })
1829 .align_items(vert_align)
1830 .interaction_source(&*li_source)
1831 .then(config.modifier);
1832
1833 if config.tonal_elevation > 0.0 {
1834 modifier = modifier.state_elevation(StateElevation {
1835 default: config.tonal_elevation,
1836 hovered: config.tonal_elevation,
1837 pressed: config.tonal_elevation,
1838 disabled: 0.0,
1839 });
1840 }
1841 if config.shadow_elevation > 0.0 {
1842 modifier = modifier.shadow(config.shadow_elevation, 0.0);
1843 }
1844
1845 if on_click.is_some() || on_long_click.is_some() {
1846 modifier = modifier.clickable();
1847 if let Some(cb) = on_click {
1848 let cb = cb.clone();
1849 modifier = modifier.on_click(move || {
1850 if is_enabled {
1851 cb();
1852 }
1853 });
1854 }
1855 if let Some(cb) = &on_long_click {
1856 let cb = cb.clone();
1857 modifier = modifier.on_long_click(move || {
1858 if is_enabled {
1859 cb();
1860 }
1861 });
1862 }
1863 }
1864
1865 let wrap_icon = |color: Color, v: View| -> View { with_content_color(color, move || v) };
1866
1867 Row(modifier).child((
1868 leading
1869 .map(|v| {
1870 Box(Modifier::new().padding_values(PaddingValues {
1871 left: 0.0,
1872 right: 16.0,
1873 top: 0.0,
1874 bottom: 0.0,
1875 }))
1876 .child(wrap_icon(ld_col, v))
1877 })
1878 .unwrap_or(Box(Modifier::new())),
1879 Column(
1880 Modifier::new()
1881 .flex_grow(1.0)
1882 .justify_content(JustifyContent::CENTER),
1883 )
1884 .child((
1885 overline_text
1886 .map(|ot| {
1887 Text(ot)
1888 .color(ol_col)
1889 .size(th.typography.label_small)
1890 .single_line()
1891 })
1892 .unwrap_or(Box(Modifier::new())),
1893 Text(headline)
1894 .color(hd_col)
1895 .size(th.typography.body_large)
1896 .single_line(),
1897 supporting_text
1898 .map(|st| {
1899 Text(st)
1900 .color(sp_col)
1901 .size(th.typography.body_medium)
1902 .max_lines(2)
1903 .overflow_ellipsize()
1904 })
1905 .unwrap_or(Box(Modifier::new())),
1906 )),
1907 trailing
1908 .map(|v| {
1909 Box(Modifier::new().padding_values(PaddingValues {
1910 left: 16.0,
1911 right: 0.0,
1912 top: 0.0,
1913 bottom: 0.0,
1914 }))
1915 .child(wrap_icon(tr_col, v))
1916 })
1917 .unwrap_or(Box(Modifier::new())),
1918 ))
1919}
1920
1921pub fn SelectableListItem(
1924 headline: impl Into<String>,
1925 selected: bool,
1926 supporting_text: Option<String>,
1927 overline_text: Option<String>,
1928 leading: Option<View>,
1929 trailing: Option<View>,
1930 on_click: Option<Rc<dyn Fn()>>,
1931 mut config: ListItemConfig,
1932) -> View {
1933 config.selected = selected;
1934 let mut m = Modifier::new().semantics(Semantics::new(Role::RadioButton));
1935 m = m.then(config.modifier);
1936 config.modifier = m;
1937 ListItem(
1938 headline,
1939 supporting_text,
1940 overline_text,
1941 leading,
1942 trailing,
1943 on_click,
1944 None,
1945 config,
1946 )
1947}
1948
1949pub fn ToggleableListItem(
1952 headline: impl Into<String>,
1953 checked: bool,
1954 on_checked_change: impl Fn(bool) + 'static,
1955 supporting_text: Option<String>,
1956 overline_text: Option<String>,
1957 leading: Option<View>,
1958 trailing: Option<View>,
1959 config: ListItemConfig,
1960) -> View {
1961 let mut cfg = config.clone();
1962 cfg.selected = checked;
1963 let cb = Rc::new(on_checked_change);
1964 let cb2 = cb.clone();
1965 let mut m = Modifier::new().semantics(Semantics::new(Role::Checkbox));
1966 m = m.then(cfg.modifier);
1967 cfg.modifier = m;
1968 ListItem(
1969 headline,
1970 supporting_text,
1971 overline_text,
1972 leading,
1973 trailing,
1974 Some(Rc::new(move || (cb2)(!checked))),
1975 None,
1976 cfg,
1977 )
1978}
1979
1980fn segmented_item_radii(index: usize, count: usize, r: f32) -> [f32; 4] {
1982 if count <= 1 {
1983 [r, r, r, r]
1984 } else if index == 0 {
1985 [0.0, 0.0, r, r]
1986 } else if index == count - 1 {
1987 [r, r, 0.0, 0.0]
1988 } else {
1989 [0.0, 0.0, 0.0, 0.0]
1990 }
1991}
1992
1993pub fn SegmentedListItem(
1995 index: usize,
1996 count: usize,
1997 headline: impl Into<String>,
1998 supporting_text: Option<String>,
1999 overline_text: Option<String>,
2000 leading: Option<View>,
2001 trailing: Option<View>,
2002 on_click: Option<Rc<dyn Fn()>>,
2003 mut config: ListItemConfig,
2004) -> View {
2005 config.shape_radii = Some(segmented_item_radii(index, count, config.shape_radius));
2006 ListItem(
2007 headline,
2008 supporting_text,
2009 overline_text,
2010 leading,
2011 trailing,
2012 on_click,
2013 None,
2014 config,
2015 )
2016}
2017
2018pub fn SegmentedSelectableListItem(
2020 index: usize,
2021 count: usize,
2022 headline: impl Into<String>,
2023 selected: bool,
2024 supporting_text: Option<String>,
2025 overline_text: Option<String>,
2026 leading: Option<View>,
2027 trailing: Option<View>,
2028 on_click: Option<Rc<dyn Fn()>>,
2029 mut config: ListItemConfig,
2030) -> View {
2031 config.selected = selected;
2032 config.shape_radii = Some(segmented_item_radii(index, count, config.shape_radius));
2033 let mut m = Modifier::new().semantics(Semantics::new(Role::RadioButton));
2034 m = m.then(config.modifier);
2035 config.modifier = m;
2036 ListItem(
2037 headline,
2038 supporting_text,
2039 overline_text,
2040 leading,
2041 trailing,
2042 on_click,
2043 None,
2044 config,
2045 )
2046}
2047
2048pub fn SegmentedToggleableListItem(
2050 index: usize,
2051 count: usize,
2052 headline: impl Into<String>,
2053 checked: bool,
2054 on_checked_change: impl Fn(bool) + 'static,
2055 supporting_text: Option<String>,
2056 overline_text: Option<String>,
2057 leading: Option<View>,
2058 trailing: Option<View>,
2059 config: ListItemConfig,
2060) -> View {
2061 let mut cfg = config.clone();
2062 cfg.selected = checked;
2063 cfg.shape_radii = Some(segmented_item_radii(index, count, cfg.shape_radius));
2064 let cb2 = Rc::new(on_checked_change);
2065 let mut m = Modifier::new().semantics(Semantics::new(Role::Checkbox));
2066 m = m.then(cfg.modifier);
2067 cfg.modifier = m;
2068 ListItem(
2069 headline,
2070 supporting_text,
2071 overline_text,
2072 leading,
2073 trailing,
2074 Some(Rc::new(move || (cb2)(!checked))),
2075 None,
2076 cfg,
2077 )
2078}
2079
2080pub struct Tab {
2082 pub label: String,
2083 pub icon: Option<View>,
2084 pub on_click: Rc<dyn Fn()>,
2085 pub enabled: bool,
2086 pub interaction_source: Option<MutableInteractionSource>,
2087}
2088
2089#[derive(Clone, Debug)]
2091pub struct TabRowConfig {
2092 pub modifier: Modifier,
2093 pub container_color: Color,
2094 pub selected_content_color: Color,
2095 pub unselected_content_color: Color,
2096 pub indicator_color: Color,
2097 pub height: f32,
2098 pub indicator_height: f32,
2099}
2100
2101impl Default for TabRowConfig {
2102 fn default() -> Self {
2103 Self {
2104 modifier: Modifier::new(),
2105 container_color: TabDefaults::container_color(),
2106 selected_content_color: TabDefaults::selected_content_color(),
2107 unselected_content_color: TabDefaults::unselected_content_color(),
2108 indicator_color: TabDefaults::indicator_color(),
2109 height: TabDefaults::HEIGHT,
2110 indicator_height: TabDefaults::INDICATOR_HEIGHT,
2111 }
2112 }
2113}
2114
2115static TABROW_COUNTER: AtomicU64 = AtomicU64::new(0);
2116
2117pub fn TabRow(selected_index: usize, tabs: Vec<Tab>, config: TabRowConfig) -> View {
2121 let th = theme();
2122 let id = remember(|| TABROW_COUNTER.fetch_add(1, Ordering::Relaxed));
2123 let default_effects = AnimationSpec::spring_crit(40.0);
2124 Column(Modifier::new().fill_max_width().then(config.modifier)).child((
2125 Row(Modifier::new()
2126 .fill_max_width()
2127 .height(config.height)
2128 .background(config.container_color)
2129 .semantics(Semantics::new(Role::Container).with_selectable_group()))
2130 .child(
2131 tabs.into_iter()
2132 .enumerate()
2133 .map(|(i, tab)| {
2134 let selected = i == selected_index;
2135 let is_enabled = tab.enabled;
2136 let color = animate_color(
2137 format!("tab_clr_{}_{}", id, i),
2138 if selected {
2139 config.selected_content_color
2140 } else {
2141 config.unselected_content_color
2142 },
2143 default_effects,
2144 );
2145 let indicator_h = animate_f32(
2146 format!("tab_ind_h_{}_{}", id, i),
2147 if selected {
2148 config.indicator_height
2149 } else {
2150 0.0
2151 },
2152 default_effects,
2153 );
2154 let cb = tab.on_click.clone();
2155 let tab_source: Rc<MutableInteractionSource> = tab
2156 .interaction_source
2157 .clone()
2158 .map(Rc::new)
2159 .unwrap_or_else(|| remember(MutableInteractionSource::new));
2160
2161 let mut tab_m = Modifier::new()
2162 .flex_grow(1.0)
2163 .fill_max_height()
2164 .interaction_source(&*tab_source)
2165 .align_items(AlignItems::CENTER)
2166 .justify_content(JustifyContent::CENTER)
2167 .state_colors(StateColors {
2168 default: Color::TRANSPARENT,
2169 hovered: th.on_surface.with_alpha_f32(0.08),
2170 pressed: th.on_surface.with_alpha_f32(0.12),
2171 disabled: Color::TRANSPARENT,
2172 })
2173 .semantics(Semantics::new(Role::Tab).with_label(&tab.label));
2174
2175 if is_enabled {
2176 tab_m = tab_m.clickable().on_click(move || cb());
2177 }
2178
2179 Column(tab_m).child((
2180 tab.icon.unwrap_or(Box(Modifier::new())),
2181 Text(tab.label)
2182 .color(color)
2183 .size(th.typography.title_small)
2184 .single_line(),
2185 Box(Modifier::new()
2186 .fill_max_width()
2187 .height(indicator_h)
2188 .background(config.indicator_color)
2189 .clip_rounded(TabDefaults::INDICATOR_CORNER)),
2190 ))
2191 })
2192 .collect::<Vec<_>>(),
2193 ),
2194 Box(Modifier::new()
2196 .fill_max_width()
2197 .height(1.0)
2198 .background(th.outline_variant)),
2199 ))
2200}
2201
2202#[derive(Clone)]
2204pub struct SegmentConfig {
2205 pub label: String,
2206 pub icon: Option<View>,
2207 pub on_click: Rc<dyn Fn()>,
2208 pub enabled: bool,
2209 pub interaction_source: Option<MutableInteractionSource>,
2210}
2211
2212impl Default for SegmentConfig {
2213 fn default() -> Self {
2214 Self {
2215 label: String::new(),
2216 icon: None,
2217 on_click: Rc::new(|| {}),
2218 enabled: true,
2219 interaction_source: None,
2220 }
2221 }
2222}
2223
2224#[derive(Clone, Debug)]
2226pub struct SegmentedButtonConfig {
2227 pub modifier: Modifier,
2228 pub border_color: Color,
2229 pub selected_container_color: Color,
2230 pub selected_content_color: Color,
2231 pub unselected_content_color: Color,
2232 pub state_colors: StateColors,
2233 pub height: f32,
2234 pub shape_radius: f32,
2235 pub content_padding: PaddingValues,
2236}
2237
2238impl Default for SegmentedButtonConfig {
2239 fn default() -> Self {
2240 Self {
2241 modifier: Modifier::new(),
2242 border_color: SegmentedButtonDefaults::border_color(),
2243 selected_container_color: SegmentedButtonDefaults::selected_container_color(),
2244 selected_content_color: SegmentedButtonDefaults::selected_content_color(),
2245 unselected_content_color: SegmentedButtonDefaults::unselected_content_color(),
2246 state_colors: SegmentedButtonDefaults::state_colors_default(),
2247 height: SegmentedButtonDefaults::HEIGHT,
2248 shape_radius: SegmentedButtonDefaults::SHAPE_RADIUS,
2249 content_padding: SegmentedButtonDefaults::CONTENT_PADDING,
2250 }
2251 }
2252}
2253
2254static SEGBUTTON_COUNTER: AtomicU64 = AtomicU64::new(0);
2255
2256pub fn SegmentedButton(
2261 selected: &[usize],
2262 segments: Vec<SegmentConfig>,
2263 config: SegmentedButtonConfig,
2264) -> View {
2265 let th = theme();
2266 let count = segments.len();
2267 let id = remember(|| SEGBUTTON_COUNTER.fetch_add(1, Ordering::Relaxed));
2268 let spec = th.motion.color;
2269 let shape_r = config.shape_radius;
2270
2271 let segment_radii = |i: usize| -> [f32; 4] {
2273 if count == 1 {
2274 [shape_r, shape_r, shape_r, shape_r]
2275 } else if i == 0 {
2276 [shape_r, 0.0, 0.0, shape_r]
2277 } else if i == count - 1 {
2278 [0.0, shape_r, shape_r, 0.0]
2279 } else {
2280 [0.0, 0.0, 0.0, 0.0]
2281 }
2282 };
2283
2284 Row(Modifier::new()
2286 .height(config.height)
2287 .border(1.0, config.border_color, shape_r)
2288 .then(config.modifier))
2289 .child(
2290 segments
2291 .into_iter()
2292 .enumerate()
2293 .map(|(i, seg)| {
2294 let is_selected = selected.contains(&i);
2295
2296 let bg = animate_color(
2297 format!("sb_bg_{}_{}", id, i),
2298 if is_selected {
2299 config.selected_container_color
2300 } else {
2301 Color::TRANSPARENT
2302 },
2303 spec,
2304 );
2305 let fg = animate_color(
2306 format!("sb_fg_{}_{}", id, i),
2307 if is_selected {
2308 config.selected_content_color
2309 } else {
2310 config.unselected_content_color
2311 },
2312 spec,
2313 );
2314
2315 let cb = seg.on_click.clone();
2316 let radii = segment_radii(i);
2317 let is_enabled = seg.enabled;
2318 let seg_source: Rc<MutableInteractionSource> = seg
2319 .interaction_source
2320 .clone()
2321 .map(Rc::new)
2322 .unwrap_or_else(|| remember(MutableInteractionSource::new));
2323
2324 let state_colors = config.state_colors;
2325 let content_modifier = Modifier::new()
2326 .flex_grow(1.0)
2327 .fill_max_height()
2328 .clip_rounded_radii(radii)
2329 .background(bg)
2330 .state_colors(state_colors)
2331 .interaction_source(&*seg_source)
2332 .align_items(AlignItems::CENTER)
2333 .justify_content(JustifyContent::CENTER)
2334 .padding_values(config.content_padding);
2335
2336 let content_modifier = if is_enabled {
2337 content_modifier.clickable().on_click(move || cb())
2338 } else {
2339 content_modifier
2340 };
2341
2342 Row(Modifier::new().flex_grow(1.0).fill_max_height()).child((
2343 Row(content_modifier).child((
2344 seg.icon.unwrap_or(Box(Modifier::new())),
2345 Text(seg.label)
2346 .color(fg)
2347 .size(th.typography.label_large)
2348 .single_line(),
2349 )),
2350 if i < count - 1 {
2351 Box(Modifier::new()
2352 .width(1.0)
2353 .fill_max_height()
2354 .background(th.outline))
2355 } else {
2356 Box(Modifier::new())
2357 },
2358 ))
2359 })
2360 .collect::<Vec<_>>(),
2361 )
2362}
2363
2364#[derive(Clone, Debug)]
2366pub struct CircularProgressIndicatorConfig {
2367 pub modifier: Modifier,
2368 pub color: Color,
2369 pub track_color: Color,
2370 pub stroke_width: f32,
2371 pub stroke_cap: StrokeCap,
2372 pub gap_size: f32,
2373}
2374
2375impl Default for CircularProgressIndicatorConfig {
2376 fn default() -> Self {
2377 Self {
2378 modifier: Modifier::new(),
2379 color: ProgressIndicatorDefaults::circular_color(),
2380 track_color: ProgressIndicatorDefaults::circular_track_color(),
2381 stroke_width: ProgressIndicatorDefaults::CIRCULAR_STROKE_WIDTH,
2382 stroke_cap: StrokeCap::Round,
2383 gap_size: 0.0,
2384 }
2385 }
2386}
2387
2388pub fn CircularProgressIndicator(
2393 value: Option<f32>,
2394 config: CircularProgressIndicatorConfig,
2395) -> View {
2396 let sz = dp_to_px(ProgressIndicatorDefaults::CIRCULAR_INDICATOR_SIZE);
2397 let stroke_px = dp_to_px(config.stroke_width);
2398 let val = value.map(|v| v.clamp(0.0, 1.0));
2399
2400 let (global_rotation, additional_rotation, sweep_val) = if value.is_none() {
2405 let shared = remember_state_with_key("circ_ind_shared", || {
2406 let mut a = AnimatedValue::new(
2407 0.0f32,
2408 AnimationSpec::tween(Duration::from_millis(6000), Easing::Linear)
2409 .repeated(RepeatableSpec::infinite()),
2410 );
2411 a.set_target(1.0);
2412 a
2413 });
2414 let mut s = shared.borrow_mut();
2415 s.update();
2416 let t = *s.get();
2417 drop(s);
2418
2419 let gv = t * 1080.0;
2420
2421 let emph = Easing::Custom(CubicBezier::new(0.05, 0.7, 0.1, 1.0));
2422 let add_kf = remember_state_with_key("circ_ind_add_kf", || KeyframesSpec {
2423 keyframes: vec![
2424 (0.0, 0.0, None),
2425 (0.05, 90.0, Some(emph)),
2426 (0.25, 90.0, None),
2427 (0.30, 180.0, None),
2428 (0.50, 180.0, None),
2429 (0.55, 270.0, None),
2430 (0.75, 270.0, None),
2431 (0.80, 360.0, None),
2432 (1.0, 360.0, None),
2433 ],
2434 });
2435 let av = add_kf.borrow().evaluate(t);
2436
2437 let std_dec = Easing::Custom(CubicBezier::new(0.2, 0.0, 0.0, 1.0));
2438 let sweep_kf = remember_state_with_key("circ_ind_sweep_kf", || KeyframesSpec {
2439 keyframes: vec![
2440 (0.0, 0.1, None),
2441 (0.5, 0.87, Some(std_dec)),
2442 (1.0, 0.1, None),
2443 ],
2444 });
2445 let sv = sweep_kf.borrow().evaluate(t);
2446
2447 (gv, av, sv)
2448 } else {
2449 (0.0, 0.0, 0.0)
2450 };
2451
2452 let indicator_size_dp = ProgressIndicatorDefaults::CIRCULAR_INDICATOR_SIZE;
2454 let adjusted_gap_dp = if config.stroke_cap == StrokeCap::Butt {
2455 config.gap_size
2456 } else {
2457 config.gap_size + config.stroke_width
2458 };
2459 let circle_dia_dp = indicator_size_dp - config.stroke_width;
2460 let gap_sweep_rad = 2.0 * adjusted_gap_dp / circle_dia_dp;
2461
2462 Box(Modifier::new().size(sz, sz).then(config.modifier).painter(
2463 move |scene: &mut Scene, rect: Rect, alpha: f32| {
2464 let mul_c = |c: Color| {
2465 Color(
2466 c.0,
2467 c.1,
2468 c.2,
2469 ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
2470 )
2471 };
2472 let cx = rect.x + rect.w * 0.5;
2473 let cy = rect.y + rect.h * 0.5;
2474 let r = (rect.w.min(rect.h)) * 0.5 - stroke_px * 0.5;
2475 let circle = Rect {
2476 x: cx - r,
2477 y: cy - r,
2478 w: r * 2.0,
2479 h: r * 2.0,
2480 };
2481
2482 match val {
2483 Some(p) => {
2484 let sweep_rad = p * std::f32::consts::TAU;
2485 let start_angle = -std::f32::consts::FRAC_PI_2;
2486 let effective_gap = gap_sweep_rad.min(sweep_rad);
2487
2488 if p > 0.0 {
2490 scene.nodes.push(SceneNode::Arc {
2491 rect: circle,
2492 start_angle,
2493 sweep_angle: sweep_rad,
2494 stroke_width: stroke_px,
2495 color: mul_c(config.color),
2496 cap: config.stroke_cap,
2497 });
2498 }
2499
2500 let track_start = start_angle + sweep_rad + effective_gap;
2502 let track_sweep = std::f32::consts::TAU - sweep_rad - 2.0 * effective_gap;
2503 if track_sweep > 0.0 {
2504 scene.nodes.push(SceneNode::Arc {
2505 rect: circle,
2506 start_angle: track_start,
2507 sweep_angle: track_sweep,
2508 stroke_width: stroke_px,
2509 color: mul_c(config.track_color),
2510 cap: config.stroke_cap,
2511 });
2512 }
2513 }
2514 None => {
2515 let radians =
2516 (global_rotation + additional_rotation) * std::f32::consts::PI / 180.0;
2517 let start_angle = -std::f32::consts::FRAC_PI_2 + radians;
2518 let sweep_rad = sweep_val * std::f32::consts::TAU;
2519 let effective_gap = gap_sweep_rad.min(sweep_rad);
2520
2521 scene.nodes.push(SceneNode::Arc {
2523 rect: circle,
2524 start_angle,
2525 sweep_angle: sweep_rad,
2526 stroke_width: stroke_px,
2527 color: mul_c(config.color),
2528 cap: config.stroke_cap,
2529 });
2530
2531 let track_start = start_angle + sweep_rad + effective_gap;
2533 let track_sweep = std::f32::consts::TAU - sweep_rad - 2.0 * effective_gap;
2534 if track_sweep > 0.0 {
2535 scene.nodes.push(SceneNode::Arc {
2536 rect: circle,
2537 start_angle: track_start,
2538 sweep_angle: track_sweep,
2539 stroke_width: stroke_px,
2540 color: mul_c(config.track_color),
2541 cap: config.stroke_cap,
2542 });
2543 }
2544 }
2545 }
2546 },
2547 ))
2548 .semantics(Semantics {
2549 role: Role::ProgressBar,
2550 label: None,
2551 focused: false,
2552 enabled: true,
2553 selectable_group: false,
2554 })
2555}
2556
2557#[derive(Clone, Debug)]
2559pub struct LinearProgressIndicatorConfig {
2560 pub modifier: Modifier,
2561 pub color: Color,
2562 pub track_color: Color,
2563 pub stroke_cap: StrokeCap,
2565 pub gap_size: f32,
2567 pub stop_size: f32,
2569}
2570
2571impl Default for LinearProgressIndicatorConfig {
2572 fn default() -> Self {
2573 Self {
2574 modifier: Modifier::new(),
2575 color: ProgressIndicatorDefaults::linear_color(),
2576 track_color: ProgressIndicatorDefaults::linear_track_color(),
2577 stroke_cap: StrokeCap::Round,
2578 gap_size: ProgressIndicatorDefaults::LINEAR_INDICATOR_GAP_SIZE,
2579 stop_size: ProgressIndicatorDefaults::LINEAR_TRACK_STOP_SIZE,
2580 }
2581 }
2582}
2583
2584pub fn LinearProgressIndicator(value: Option<f32>, config: LinearProgressIndicatorConfig) -> View {
2589 Box(Modifier::new()
2590 .fill_max_width()
2591 .height(ProgressIndicatorDefaults::LINEAR_INDICATOR_HEIGHT)
2592 .then(config.modifier)
2593 .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
2594 let mul_c = |c: Color| {
2595 Color(
2596 c.0,
2597 c.1,
2598 c.2,
2599 ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
2600 )
2601 };
2602 let track_h = rect.h;
2603 let corner = track_h * 0.5;
2604 let dot_r = dp_to_px(config.stop_size) * 0.5;
2605 let cy = rect.y + rect.h * 0.5;
2606 let t = value.unwrap_or(0.0).clamp(0.0, 1.0);
2607
2608 let cap_radius = if config.stroke_cap == StrokeCap::Butt {
2609 0.0
2610 } else {
2611 corner
2612 };
2613
2614 let gap = dp_to_px(config.gap_size)
2615 - if config.stroke_cap == StrokeCap::Butt {
2616 0.0
2617 } else {
2618 cap_radius
2619 };
2620
2621 let cap_ofs = cap_radius;
2622 let ind_end = (t * rect.w).clamp(cap_ofs, rect.w - cap_ofs);
2623 let ind_w = (ind_end - cap_ofs).max(0.0);
2624
2625 if t > 0.0 && ind_w > 0.0 {
2627 scene.nodes.push(SceneNode::Rect {
2628 rect: Rect {
2629 x: rect.x + cap_ofs,
2630 y: cy - corner,
2631 w: ind_w,
2632 h: track_h,
2633 },
2634 brush: Brush::Solid(mul_c(config.color)),
2635 radius: [cap_radius; 4],
2636 });
2637 }
2638
2639 let track_start = (rect.x + ind_end + gap).min(rect.x + rect.w);
2641 let track_w = (rect.x + rect.w - track_start).max(0.0);
2642 if t < 1.0 && track_w > 0.0 {
2643 let track_left = track_start + cap_ofs;
2644 let track_right = rect.x + rect.w;
2645 if track_right > track_left {
2646 scene.nodes.push(SceneNode::Rect {
2647 rect: Rect {
2648 x: track_left,
2649 y: cy - corner,
2650 w: track_right - track_left,
2651 h: track_h,
2652 },
2653 brush: Brush::Solid(mul_c(config.track_color)),
2654 radius: [cap_radius; 4],
2655 });
2656 }
2657 }
2658
2659 {
2661 let sx = rect.x + rect.w - dot_r;
2662 scene.nodes.push(SceneNode::Ellipse {
2663 rect: Rect {
2664 x: sx - dot_r,
2665 y: cy - dot_r,
2666 w: dot_r * 2.0,
2667 h: dot_r * 2.0,
2668 },
2669 brush: Brush::Solid(mul_c(config.color)),
2670 });
2671 }
2672 }))
2673 .semantics(Semantics {
2674 role: Role::ProgressBar,
2675 label: None,
2676 focused: false,
2677 enabled: true,
2678 selectable_group: false,
2679 })
2680}
2681
2682#[allow(dead_code)]
2685#[derive(Clone, Debug)]
2686pub struct TextFieldColors {
2687 pub focused_text_color: Color,
2688 pub unfocused_text_color: Color,
2689 pub disabled_text_color: Color,
2690 pub error_text_color: Color,
2691 pub focused_container_color: Color,
2692 pub unfocused_container_color: Color,
2693 pub disabled_container_color: Color,
2694 pub error_container_color: Color,
2695 pub cursor_color: Color,
2696 pub error_cursor_color: Color,
2697 pub focused_indicator_color: Color,
2698 pub unfocused_indicator_color: Color,
2699 pub disabled_indicator_color: Color,
2700 pub error_indicator_color: Color,
2701 pub focused_leading_icon_color: Color,
2702 pub unfocused_leading_icon_color: Color,
2703 pub disabled_leading_icon_color: Color,
2704 pub error_leading_icon_color: Color,
2705 pub focused_trailing_icon_color: Color,
2706 pub unfocused_trailing_icon_color: Color,
2707 pub disabled_trailing_icon_color: Color,
2708 pub error_trailing_icon_color: Color,
2709 pub focused_label_color: Color,
2710 pub unfocused_label_color: Color,
2711 pub disabled_label_color: Color,
2712 pub error_label_color: Color,
2713 pub focused_placeholder_color: Color,
2714 pub unfocused_placeholder_color: Color,
2715 pub disabled_placeholder_color: Color,
2716 pub error_placeholder_color: Color,
2717 pub focused_supporting_text_color: Color,
2718 pub unfocused_supporting_text_color: Color,
2719 pub disabled_supporting_text_color: Color,
2720 pub error_supporting_text_color: Color,
2721 pub focused_prefix_color: Color,
2722 pub unfocused_prefix_color: Color,
2723 pub disabled_prefix_color: Color,
2724 pub error_prefix_color: Color,
2725 pub focused_suffix_color: Color,
2726 pub unfocused_suffix_color: Color,
2727 pub disabled_suffix_color: Color,
2728 pub error_suffix_color: Color,
2729}
2730
2731#[allow(dead_code)]
2732impl TextFieldColors {
2733 pub fn text_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2734 if !enabled {
2735 self.disabled_text_color
2736 } else if is_error {
2737 self.error_text_color
2738 } else if focused {
2739 self.focused_text_color
2740 } else {
2741 self.unfocused_text_color
2742 }
2743 }
2744 pub fn container_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2745 if !enabled {
2746 self.disabled_container_color
2747 } else if is_error {
2748 self.error_container_color
2749 } else if focused {
2750 self.focused_container_color
2751 } else {
2752 self.unfocused_container_color
2753 }
2754 }
2755 pub fn cursor_color(&self, is_error: bool) -> Color {
2756 if is_error {
2757 self.error_cursor_color
2758 } else {
2759 self.cursor_color
2760 }
2761 }
2762 pub fn indicator_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2763 if !enabled {
2764 self.disabled_indicator_color
2765 } else if is_error {
2766 self.error_indicator_color
2767 } else if focused {
2768 self.focused_indicator_color
2769 } else {
2770 self.unfocused_indicator_color
2771 }
2772 }
2773 pub fn leading_icon_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2774 if !enabled {
2775 self.disabled_leading_icon_color
2776 } else if is_error {
2777 self.error_leading_icon_color
2778 } else if focused {
2779 self.focused_leading_icon_color
2780 } else {
2781 self.unfocused_leading_icon_color
2782 }
2783 }
2784 pub fn trailing_icon_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2785 if !enabled {
2786 self.disabled_trailing_icon_color
2787 } else if is_error {
2788 self.error_trailing_icon_color
2789 } else if focused {
2790 self.focused_trailing_icon_color
2791 } else {
2792 self.unfocused_trailing_icon_color
2793 }
2794 }
2795 pub fn label_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2796 if !enabled {
2797 self.disabled_label_color
2798 } else if is_error {
2799 self.error_label_color
2800 } else if focused {
2801 self.focused_label_color
2802 } else {
2803 self.unfocused_label_color
2804 }
2805 }
2806 pub fn placeholder_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2807 if !enabled {
2808 self.disabled_placeholder_color
2809 } else if is_error {
2810 self.error_placeholder_color
2811 } else if focused {
2812 self.focused_placeholder_color
2813 } else {
2814 self.unfocused_placeholder_color
2815 }
2816 }
2817 pub fn supporting_text_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2818 if !enabled {
2819 self.disabled_supporting_text_color
2820 } else if is_error {
2821 self.error_supporting_text_color
2822 } else if focused {
2823 self.focused_supporting_text_color
2824 } else {
2825 self.unfocused_supporting_text_color
2826 }
2827 }
2828}
2829
2830pub struct TextFieldDefaults;
2832
2833impl TextFieldDefaults {
2834 pub const MIN_HEIGHT: f32 = 56.0;
2836 pub const MIN_WIDTH: f32 = 280.0;
2838
2839 pub fn colors() -> TextFieldColors {
2840 let th = theme();
2841 TextFieldColors {
2842 focused_text_color: th.on_surface,
2843 unfocused_text_color: th.on_surface,
2844 disabled_text_color: th.on_surface.with_alpha_f32(0.38),
2845 error_text_color: th.on_surface,
2846 focused_container_color: th.surface_container_highest,
2847 unfocused_container_color: th.surface_container_highest,
2848 disabled_container_color: th.on_surface.with_alpha_f32(0.04),
2849 error_container_color: th.surface_container_highest,
2850 cursor_color: th.primary,
2851 error_cursor_color: th.error,
2852 focused_indicator_color: th.primary,
2853 unfocused_indicator_color: th.on_surface_variant,
2854 disabled_indicator_color: th.on_surface.with_alpha_f32(0.12),
2855 error_indicator_color: th.error,
2856 focused_leading_icon_color: th.on_surface_variant,
2857 unfocused_leading_icon_color: th.on_surface_variant,
2858 disabled_leading_icon_color: th.on_surface.with_alpha_f32(0.38),
2859 error_leading_icon_color: th.error,
2860 focused_trailing_icon_color: th.on_surface_variant,
2861 unfocused_trailing_icon_color: th.on_surface_variant,
2862 disabled_trailing_icon_color: th.on_surface.with_alpha_f32(0.38),
2863 error_trailing_icon_color: th.error,
2864 focused_label_color: th.primary,
2865 unfocused_label_color: th.on_surface_variant,
2866 disabled_label_color: th.on_surface.with_alpha_f32(0.38),
2867 error_label_color: th.error,
2868 focused_placeholder_color: th.on_surface_variant,
2869 unfocused_placeholder_color: th.on_surface_variant,
2870 disabled_placeholder_color: th.on_surface.with_alpha_f32(0.38),
2871 error_placeholder_color: th.error,
2872 focused_supporting_text_color: th.on_surface_variant,
2873 unfocused_supporting_text_color: th.on_surface_variant,
2874 disabled_supporting_text_color: th.on_surface.with_alpha_f32(0.38),
2875 error_supporting_text_color: th.error,
2876 focused_prefix_color: th.on_surface,
2877 unfocused_prefix_color: th.on_surface,
2878 disabled_prefix_color: th.on_surface.with_alpha_f32(0.38),
2879 error_prefix_color: th.on_surface,
2880 focused_suffix_color: th.on_surface,
2881 unfocused_suffix_color: th.on_surface,
2882 disabled_suffix_color: th.on_surface.with_alpha_f32(0.38),
2883 error_suffix_color: th.on_surface,
2884 }
2885 }
2886}
2887
2888#[derive(Clone)]
2890pub struct OutlinedTextFieldConfig {
2891 pub label: Option<String>,
2895 pub placeholder: Option<String>,
2899 pub leading_icon: Option<View>,
2901 pub trailing_icon: Option<View>,
2903 pub single_line: bool,
2905 pub is_error: bool,
2907 pub enabled: bool,
2909 pub on_submit: Option<Rc<dyn Fn(String)>>,
2911 pub colors: Option<TextFieldColors>,
2913 pub focus_tracker: Option<Rc<Cell<bool>>>,
2917}
2918
2919impl Default for OutlinedTextFieldConfig {
2920 fn default() -> Self {
2921 Self {
2922 label: None,
2923 placeholder: None,
2924 leading_icon: None,
2925 trailing_icon: None,
2926 single_line: true,
2927 is_error: false,
2928 enabled: true,
2929 on_submit: None,
2930 colors: None,
2931 focus_tracker: None,
2932 }
2933 }
2934}
2935
2936pub fn OutlinedTextField(
2958 modifier: Modifier,
2959 value: String,
2960 on_value_change: impl Fn(String) + 'static,
2961 config: OutlinedTextFieldConfig,
2962) -> View {
2963 let label_str: Option<Rc<str>> = config.label.clone().map(Rc::from);
2964 let has_label = label_str.is_some();
2965
2966 let anim_key = match &label_str {
2968 Some(l) => format!("otf_{}", &l[..l.len().min(32)]),
2969 None => "otf_nolabel".into(),
2970 };
2971
2972 let focus_tracker: Rc<Cell<bool>> = match config.focus_tracker.clone() {
2976 Some(ft) => ft,
2977 None => remember_with_key(format!("otf_focus_{}", anim_key), || Cell::new(false)),
2978 };
2979 let is_focused = focus_tracker.get();
2980 let should_float = !value.is_empty() || is_focused;
2981
2982 let tf_placeholder = if has_label {
2983 if should_float {
2984 config.placeholder.clone().unwrap_or_default()
2985 } else {
2986 String::new()
2987 }
2988 } else {
2989 config.placeholder.clone().unwrap_or_default()
2990 };
2991
2992 let text_input = View::new(0, ViewKind::Box)
2993 .modifier(
2994 Modifier::new().flex_grow(1.0).text_input(TextInputConfig {
2995 hint: tf_placeholder,
2996 multiline: false,
2997 on_change: Some(Rc::new(on_value_change) as _),
2998 on_submit: config.on_submit.clone().map(|f| {
2999 let f = f.clone();
3000 Rc::new(move |s| f(s)) as Rc<dyn Fn(String)>
3001 }),
3002 focus_tracker: Some(focus_tracker),
3003 value: value.clone(),
3004 visual_transformation: None,
3005 keyboard_type: Default::default(),
3006 capitalization: Default::default(),
3007 ime_action: Default::default(),
3008 enabled: config.enabled,
3009 read_only: false,
3010 max_lines: None,
3011 min_lines: 1,
3012 cursor_color: config
3013 .colors
3014 .as_ref()
3015 .map(|c| c.cursor_color(config.is_error)),
3016 on_text_layout: None,
3017 text_style: None,
3018 keyboard_actions: None,
3019 interaction_source: None,
3020 line_limits: None,
3021 }),
3022 )
3023 .semantics(Semantics {
3024 role: Role::TextField,
3025 label: None,
3026 focused: false,
3027 enabled: true,
3028 selectable_group: false,
3029 });
3030
3031 outlined_field_decoration(
3032 modifier,
3033 anim_key,
3034 label_str,
3035 &config,
3036 is_focused,
3037 !value.is_empty(),
3038 text_input,
3039 )
3040}
3041
3042pub fn OutlinedTextFieldState(
3044 modifier: Modifier,
3045 state: Rc<RefCell<TextFieldState>>,
3046 on_value_change: impl Fn(String) + 'static,
3047 config: OutlinedTextFieldConfig,
3048) -> View {
3049 let label_str: Option<Rc<str>> = config.label.clone().map(Rc::from);
3050 let has_label = label_str.is_some();
3051
3052 let anim_key = match &label_str {
3054 Some(l) => format!("otf_{}", &l[..l.len().min(32)]),
3055 None => "otf_nolabel".into(),
3056 };
3057
3058 let focus_tracker: Rc<Cell<bool>> = match config.focus_tracker.clone() {
3059 Some(ft) => ft,
3060 None => remember_with_key(format!("otf_focus_{}", anim_key), || Cell::new(false)),
3061 };
3062 let is_focused = focus_tracker.get();
3063 let has_content = !state.borrow().text.is_empty();
3064 let should_float = has_content || is_focused;
3065
3066 let tf_placeholder = if has_label {
3068 if should_float {
3069 config.placeholder.clone().unwrap_or_default()
3070 } else {
3071 String::new()
3072 }
3073 } else {
3074 config.placeholder.clone().unwrap_or_default()
3075 };
3076
3077 let text_input = BasicTextField(
3078 state,
3079 Modifier::new().flex_grow(1.0),
3080 tf_placeholder,
3081 BasicTextFieldConfig {
3082 line_limits: if config.single_line {
3083 TextFieldLineLimits::SingleLine
3084 } else {
3085 TextFieldLineLimits::MultiLine {
3086 min_height_in_lines: 1,
3087 max_height_in_lines: usize::MAX,
3088 }
3089 },
3090 on_change: Some(Rc::new(on_value_change)),
3091 on_submit: config.on_submit.clone(),
3092 focus_tracker: Some(focus_tracker),
3093 enabled: config.enabled,
3094 ..Default::default()
3095 },
3096 );
3097
3098 outlined_field_decoration(
3099 modifier,
3100 anim_key,
3101 label_str,
3102 &config,
3103 is_focused,
3104 has_content,
3105 text_input,
3106 )
3107}
3108
3109fn outlined_field_decoration(
3110 modifier: Modifier,
3111 anim_key: String,
3112 label_str: Option<Rc<str>>,
3113 config: &OutlinedTextFieldConfig,
3114 is_focused: bool,
3115 has_content: bool,
3116 text_input: View,
3117) -> View {
3118 let th = theme();
3119 let has_label = label_str.is_some();
3120
3121 let should_float = has_content || is_focused;
3122 let float_t = animate_f32(
3123 anim_key.clone(),
3124 if should_float { 1.0 } else { 0.0 },
3125 th.motion.color,
3126 );
3127
3128 let target_border_w = if config.is_error || should_float {
3129 OutlinedTextFieldDefaults::FOCUSED_BORDER_THICKNESS
3130 } else {
3131 OutlinedTextFieldDefaults::UNFOCUSED_BORDER_THICKNESS
3132 };
3133 let border_w = animate_f32(
3134 format!("otf_bw_{}", anim_key),
3135 target_border_w,
3136 th.motion.color,
3137 );
3138
3139 let (border_color, label_color, container_bg) = if let Some(ref tc) = config.colors {
3140 (
3141 tc.indicator_color(config.enabled, config.is_error, is_focused),
3142 tc.label_color(config.enabled, config.is_error, is_focused),
3143 tc.container_color(config.enabled, config.is_error, is_focused),
3144 )
3145 } else {
3146 (
3147 if config.is_error {
3148 th.error
3149 } else if is_focused {
3150 th.primary
3151 } else {
3152 th.outline
3153 },
3154 if config.is_error {
3155 th.error
3156 } else if is_focused {
3157 th.primary
3158 } else {
3159 th.on_surface_variant
3160 },
3161 th.surface,
3162 )
3163 };
3164
3165 let label_size = 16.0 - 4.0 * float_t;
3167
3168 let min_label_half_h: f32 = if has_label { 8.0 } else { 0.0 };
3170
3171 let label_start_y = (56.0 - 16.0) / 2.0;
3173 let label_end_y = -min_label_half_h;
3174 let label_y = label_start_y - (label_start_y - label_end_y) * float_t;
3175
3176 let label_start_x = if has_label { 24.0 } else { 0.0 };
3178 let label_end_x = if has_label { 20.0 } else { 0.0 };
3179 let label_x = label_start_x - (label_start_x - label_end_x) * float_t;
3180
3181 let (top_pad, bottom_pad) = if has_label { (8.0, 8.0) } else { (16.0, 16.0) };
3183
3184 let label_cutout = label_str.as_ref().map(|lbl| {
3187 let font_px = dp_to_px(label_size) * repose_core::locals::text_scale().0;
3188 let m = measure_text(lbl, font_px, TextMeasureConfig::default());
3189 let text_width_px = m.positions.last().copied().unwrap_or(0.0);
3190 let text_width_dp = px_to_dp(text_width_px);
3191 let pad = 1.0;
3192 let line_h = 16.0;
3193 (
3194 label_x - pad,
3195 label_y - pad,
3196 label_x + text_width_dp + pad,
3197 label_y + line_h + pad,
3198 )
3199 });
3200
3201 ZStack(
3202 modifier
3203 .min_height(OutlinedTextFieldDefaults::MIN_HEIGHT)
3204 .min_width(OutlinedTextFieldDefaults::MIN_WIDTH),
3205 )
3206 .child((
3207 Box(Modifier::new()
3209 .fill_max_size()
3210 .clip_rounded(th.shapes.small)
3211 .background(container_bg)),
3212 if has_label {
3214 let mut bm = Modifier::new()
3215 .fill_max_size()
3216 .clip_rounded(th.shapes.small)
3217 .border(border_w, border_color, th.shapes.small);
3218 if let Some((l, t, r, b)) = label_cutout {
3219 bm = bm.clip_rect(l, t, r, b, ClipOp::Difference);
3220 }
3221 Box(bm)
3222 } else {
3223 Box(Modifier::new()
3224 .fill_max_size()
3225 .clip_rounded(th.shapes.small)
3226 .border(border_w, border_color, th.shapes.small))
3227 },
3228 Row(Modifier::new()
3230 .fill_max_size()
3231 .padding_values(PaddingValues {
3232 left: 16.0,
3233 right: 16.0,
3234 top: top_pad,
3235 bottom: bottom_pad,
3236 })
3237 .align_items(AlignItems::CENTER))
3238 .child((
3239 config.leading_icon.clone().unwrap_or(Box(Modifier::new())),
3240 text_input,
3241 config.trailing_icon.clone().unwrap_or(Box(Modifier::new())),
3242 )),
3243 if let Some(lbl) = label_str {
3245 Box(Modifier::new()
3246 .min_width(200.0)
3247 .padding_values(PaddingValues {
3248 left: label_x,
3249 right: 20.0,
3250 top: 0.0,
3251 bottom: 0.0,
3252 })
3253 .absolute()
3254 .offset(Some(0.0), Some(label_y), None, None))
3255 .child(
3256 Text(lbl.as_ref().to_string())
3257 .color(label_color)
3258 .size(label_size),
3259 )
3260 } else {
3261 Box(Modifier::new())
3262 },
3263 ))
3264}
3265
3266#[derive(Clone)]
3268pub struct TextFieldConfig {
3269 pub label: Option<String>,
3270 pub placeholder: Option<String>,
3271 pub leading_icon: Option<View>,
3272 pub trailing_icon: Option<View>,
3273 pub single_line: bool,
3274 pub is_error: bool,
3275 pub enabled: bool,
3276 pub on_submit: Option<Rc<dyn Fn(String)>>,
3277 pub colors: Option<TextFieldColors>,
3278}
3279
3280impl Default for TextFieldConfig {
3281 fn default() -> Self {
3282 Self {
3283 label: None,
3284 placeholder: None,
3285 leading_icon: None,
3286 trailing_icon: None,
3287 single_line: true,
3288 is_error: false,
3289 enabled: true,
3290 on_submit: None,
3291 colors: None,
3292 }
3293 }
3294}
3295
3296pub fn TextField(
3303 modifier: Modifier,
3304 value: String,
3305 on_value_change: impl Fn(String) + 'static,
3306 config: TextFieldConfig,
3307) -> View {
3308 let th = theme();
3309 let label_str: Option<Rc<str>> = config.label.map(Rc::from);
3310 let has_label = label_str.is_some();
3311
3312 let anim_key = match &label_str {
3313 Some(l) => format!("tf_{}", &l[..l.len().min(32)]),
3314 None => "tf_nolabel".into(),
3315 };
3316
3317 let focus_tracker: Rc<Cell<bool>> =
3318 remember_with_key(format!("tf_focus_{}", anim_key), || Cell::new(false));
3319 let is_focused = focus_tracker.get();
3320 let should_float = !value.is_empty() || is_focused;
3321
3322 let float_t = animate_f32(
3323 anim_key.clone(),
3324 if should_float { 1.0 } else { 0.0 },
3325 th.motion.color,
3326 );
3327
3328 let (indicator_color, label_color, container_bg) = if let Some(ref tc) = config.colors {
3329 let enf = config.enabled && is_focused;
3330 let ind = tc.indicator_color(config.enabled, config.is_error, enf);
3331 let lb = tc.label_color(config.enabled, config.is_error, enf);
3332 let bg = tc.container_color(config.enabled, config.is_error, enf);
3333 (ind, lb, bg)
3334 } else {
3335 let ind = if config.is_error {
3336 th.error
3337 } else if float_t > 0.5 {
3338 th.primary
3339 } else {
3340 th.on_surface_variant
3341 };
3342 let lb = if config.is_error {
3343 th.error
3344 } else if float_t > 0.5 {
3345 th.primary
3346 } else {
3347 th.on_surface_variant
3348 };
3349 let bg = if config.enabled {
3350 th.surface_container_highest
3351 } else {
3352 th.on_surface
3353 .with_alpha_f32(0.04)
3354 .composite_over(th.surface)
3355 };
3356 (ind, lb, bg)
3357 };
3358
3359 let label_size = 16.0 - 4.0 * float_t;
3360
3361 let label_start_y = (56.0 - 16.0) / 2.0;
3362 let label_end_y = if has_label { 8.0 } else { 0.0 };
3363 let label_y = label_start_y - (label_start_y - label_end_y) * float_t;
3364
3365 let label_start_x = if has_label { 24.0 } else { 0.0 };
3366 let label_end_x = if has_label { 20.0 } else { 0.0 };
3367 let label_x = label_start_x - (label_start_x - label_end_x) * float_t;
3368
3369 let tf_placeholder = if has_label {
3370 if should_float {
3371 config.placeholder.unwrap_or_default()
3372 } else {
3373 String::new()
3374 }
3375 } else {
3376 config.placeholder.unwrap_or_default()
3377 };
3378
3379 let indicator_active = config.is_error || (config.enabled && is_focused);
3380 let indicator_target_w = if indicator_active { 2.0 } else { 1.0 };
3381 let indicator_w = animate_f32(
3382 format!("tf_ind_w_{}", anim_key),
3383 indicator_target_w,
3384 th.motion.color,
3385 );
3386
3387 let (top_pad, bottom_pad) = if has_label { (8.0, 8.0) } else { (16.0, 16.0) };
3388
3389 Column(
3390 modifier
3391 .min_height(TextFieldDefaults::MIN_HEIGHT)
3392 .min_width(TextFieldDefaults::MIN_WIDTH),
3393 )
3394 .child((
3395 Box(Modifier::new()
3397 .fill_max_size()
3398 .clip_rounded(th.shapes.extra_small)
3399 .background(container_bg))
3400 .child(
3401 Column(Modifier::new().fill_max_size()).child((
3402 Row(Modifier::new()
3404 .fill_max_size()
3405 .padding_values(PaddingValues {
3406 left: 16.0,
3407 right: 16.0,
3408 top: top_pad,
3409 bottom: bottom_pad,
3410 })
3411 .align_items(AlignItems::CENTER))
3412 .child((
3413 config.leading_icon.unwrap_or(Box(Modifier::new())),
3414 View::new(0, ViewKind::Box)
3415 .modifier(
3416 Modifier::new().flex_grow(1.0).text_input(TextInputConfig {
3417 hint: tf_placeholder,
3418 multiline: !config.single_line,
3419 on_change: Some(Rc::new(on_value_change) as _),
3420 on_submit: config.on_submit.clone().map(|f| {
3421 let f = f.clone();
3422 Rc::new(move |s| f(s)) as Rc<dyn Fn(String)>
3423 }),
3424 focus_tracker: Some(focus_tracker.clone()),
3425 value: value.clone(),
3426 visual_transformation: None,
3427 keyboard_type: Default::default(),
3428 capitalization: Default::default(),
3429 ime_action: Default::default(),
3430 enabled: config.enabled,
3431 read_only: false,
3432 max_lines: None,
3433 min_lines: 1,
3434 cursor_color: config
3435 .colors
3436 .as_ref()
3437 .map(|c| c.cursor_color(config.is_error)),
3438 on_text_layout: None,
3439 text_style: None,
3440 keyboard_actions: None,
3441 interaction_source: None,
3442 line_limits: None,
3443 }),
3444 )
3445 .semantics(Semantics {
3446 role: Role::TextField,
3447 label: None,
3448 focused: false,
3449 enabled: true,
3450 selectable_group: false,
3451 }),
3452 config.trailing_icon.unwrap_or(Box(Modifier::new())),
3453 )),
3454 Box(Modifier::new()
3456 .fill_max_width()
3457 .height(indicator_w)
3458 .absolute()
3459 .offset(None, None, None, Some(0.0))
3460 .background(indicator_color)),
3461 )),
3462 ),
3463 if let Some(lbl) = label_str {
3465 Box(Modifier::new()
3466 .min_width(200.0)
3467 .padding_values(PaddingValues {
3468 left: label_x,
3469 right: 20.0,
3470 top: 0.0,
3471 bottom: 0.0,
3472 })
3473 .absolute()
3474 .offset(Some(0.0), Some(label_y), None, None))
3475 .child(
3476 Text(lbl.as_ref().to_string())
3477 .color(label_color)
3478 .size(label_size),
3479 )
3480 } else {
3481 Box(Modifier::new())
3482 },
3483 ))
3484}
3485
3486#[derive(Clone, Debug)]
3488pub struct CheckboxConfig {
3489 pub modifier: Modifier,
3490 pub enabled: bool,
3492 pub checked_color: Color,
3493 pub unchecked_color: Color,
3494 pub checkmark_color: Color,
3495 pub checked_border_color: Color,
3497 pub unchecked_border_color: Color,
3499 pub disabled_checked_box_color: Color,
3500 pub disabled_unchecked_box_color: Color,
3501 pub disabled_indeterminate_box_color: Color,
3502 pub disabled_checkmark_color: Color,
3503 pub disabled_checked_border_color: Color,
3504 pub disabled_unchecked_border_color: Color,
3505 pub disabled_indeterminate_border_color: Color,
3506 pub state_colors: StateColors,
3507 pub interaction_source: Option<MutableInteractionSource>,
3508}
3509
3510impl Default for CheckboxConfig {
3511 fn default() -> Self {
3512 Self {
3513 modifier: Modifier::new(),
3514 enabled: true,
3515 checked_color: CheckboxDefaults::checked_color(),
3516 unchecked_color: CheckboxDefaults::unchecked_color(),
3517 checkmark_color: CheckboxDefaults::checkmark_color(),
3518 checked_border_color: CheckboxDefaults::checked_color(),
3519 unchecked_border_color: CheckboxDefaults::unchecked_color(),
3520 disabled_checked_box_color: CheckboxDefaults::disabled_checked_box_color(),
3521 disabled_unchecked_box_color: Color::TRANSPARENT,
3522 disabled_indeterminate_box_color: CheckboxDefaults::disabled_checked_box_color(),
3523 disabled_checkmark_color: CheckboxDefaults::disabled_checkmark_color(),
3524 disabled_checked_border_color: CheckboxDefaults::disabled_checked_box_color(),
3525 disabled_unchecked_border_color: CheckboxDefaults::disabled_unchecked_border_color(),
3526 disabled_indeterminate_border_color: CheckboxDefaults::disabled_checked_box_color(),
3527 state_colors: CheckboxDefaults::state_colors_default(),
3528 interaction_source: None,
3529 }
3530 }
3531}
3532
3533static CHECKBOX_COUNTER: AtomicU64 = AtomicU64::new(0);
3537pub fn Checkbox(checked: bool, on_change: impl Fn(bool) + 'static, config: CheckboxConfig) -> View {
3538 let th = theme();
3539 let sz = CheckboxDefaults::BOX_SIZE;
3540
3541 let id = remember(|| CHECKBOX_COUNTER.fetch_add(1, Ordering::Relaxed));
3542 let spec = th.motion.color_fast;
3543
3544 let is_enabled = config.enabled;
3545
3546 let fill = animate_color(
3547 format!("cb_fill_{}", id),
3548 if !is_enabled {
3549 if checked {
3550 config.disabled_checked_box_color
3551 } else {
3552 config.disabled_unchecked_box_color
3553 }
3554 } else if checked {
3555 config.checked_color
3556 } else {
3557 Color::TRANSPARENT
3558 },
3559 spec,
3560 );
3561 let bd_w = animate_f32(
3562 format!("cb_bw_{}", id),
3563 if !is_enabled && checked {
3564 0.0
3565 } else if !is_enabled {
3566 CheckboxDefaults::STROKE_WIDTH
3567 } else if checked {
3568 0.0
3569 } else {
3570 CheckboxDefaults::STROKE_WIDTH
3571 },
3572 spec,
3573 );
3574 let bd = animate_color(
3575 format!("cb_bd_{}", id),
3576 if !is_enabled {
3577 if checked {
3578 config.disabled_checked_border_color
3579 } else {
3580 config.disabled_unchecked_border_color
3581 }
3582 } else if checked {
3583 Color::TRANSPARENT
3584 } else {
3585 config.unchecked_border_color
3586 },
3587 spec,
3588 );
3589 let check_alpha = animate_f32(
3590 format!("cb_ca_{}", id),
3591 if checked { 1.0 } else { 0.0 },
3592 spec,
3593 );
3594 let check_col = if !is_enabled {
3595 config.disabled_checkmark_color
3596 } else {
3597 config.checkmark_color
3598 };
3599
3600 let cb = move || {
3601 if config.enabled {
3602 on_change(!checked)
3603 }
3604 };
3605
3606 let cb_source: Rc<MutableInteractionSource> = config
3607 .interaction_source
3608 .clone()
3609 .map(Rc::new)
3610 .unwrap_or_else(|| remember(MutableInteractionSource::new));
3611 Box(Modifier::new()
3612 .width(CheckboxDefaults::TOUCH_TARGET_SIZE)
3613 .height(CheckboxDefaults::TOUCH_TARGET_SIZE)
3614 .padding(0.0)
3615 .clip_rounded(20.0)
3616 .background(Color::TRANSPARENT)
3617 .state_colors(config.state_colors)
3618 .interaction_source(&*cb_source)
3619 .clickable()
3620 .align_items(AlignItems::CENTER)
3621 .justify_content(JustifyContent::CENTER)
3622 .on_click(cb)
3623 .then(config.modifier))
3624 .child(
3625 Box(Modifier::new()
3626 .size(sz, sz)
3627 .background(fill)
3628 .border(bd_w, bd, CheckboxDefaults::CORNER_RADIUS)
3629 .clip_rounded(CheckboxDefaults::CORNER_RADIUS)
3630 .align_items(AlignItems::CENTER)
3631 .justify_content(JustifyContent::CENTER))
3632 .child(if check_alpha > 0.01 {
3633 Box(Modifier::new().alpha(check_alpha)).child(
3634 Icon(Symbol::new("done", '\u{E876}'))
3635 .color(check_col)
3636 .size(CheckboxDefaults::CHECK_ICON_SIZE),
3637 )
3638 } else {
3639 Box(Modifier::new())
3640 }),
3641 )
3642}
3643
3644#[derive(Clone, Copy, Debug, PartialEq)]
3646pub enum TriState {
3647 Checked,
3648 Unchecked,
3649 Indeterminate,
3650}
3651
3652pub fn TriStateCheckbox(
3655 state: TriState,
3656 on_change: impl Fn(TriState) + 'static,
3657 config: CheckboxConfig,
3658) -> View {
3659 let th = theme();
3660 let sz = CheckboxDefaults::BOX_SIZE;
3661
3662 let id = remember(|| CHECKBOX_COUNTER.fetch_add(1, Ordering::Relaxed));
3663 let spec = th.motion.color_fast;
3664
3665 let is_checked = state == TriState::Checked;
3666 let is_indeterminate = state == TriState::Indeterminate;
3667 let has_fill = is_checked || is_indeterminate;
3668 let is_enabled = config.enabled;
3669
3670 let fill = animate_color(
3671 format!("tc_fill_{}", id),
3672 if !is_enabled {
3673 if has_fill {
3674 config.disabled_indeterminate_box_color
3675 } else {
3676 config.disabled_unchecked_box_color
3677 }
3678 } else if has_fill {
3679 config.checked_color
3680 } else {
3681 Color::TRANSPARENT
3682 },
3683 spec,
3684 );
3685 let bd_w = animate_f32(
3686 format!("tc_bw_{}", id),
3687 if !is_enabled {
3688 if has_fill {
3689 0.0
3690 } else {
3691 CheckboxDefaults::STROKE_WIDTH
3692 }
3693 } else if has_fill {
3694 0.0
3695 } else {
3696 CheckboxDefaults::STROKE_WIDTH
3697 },
3698 spec,
3699 );
3700 let bd = animate_color(
3701 format!("tc_bd_{}", id),
3702 if !is_enabled {
3703 if has_fill {
3704 config.disabled_indeterminate_border_color
3705 } else {
3706 config.disabled_unchecked_border_color
3707 }
3708 } else if has_fill {
3709 Color::TRANSPARENT
3710 } else {
3711 config.unchecked_border_color
3712 },
3713 spec,
3714 );
3715 let symbol_alpha = animate_f32(
3716 format!("tc_sa_{}", id),
3717 if has_fill { 1.0 } else { 0.0 },
3718 spec,
3719 );
3720 let symbol_col = if !is_enabled {
3721 config.disabled_checkmark_color
3722 } else {
3723 config.checkmark_color
3724 };
3725
3726 Box(Modifier::new()
3727 .width(CheckboxDefaults::TOUCH_TARGET_SIZE)
3728 .height(CheckboxDefaults::TOUCH_TARGET_SIZE)
3729 .padding(0.0)
3730 .clip_rounded(20.0)
3731 .background(Color::TRANSPARENT)
3732 .clickable()
3733 .align_items(AlignItems::CENTER)
3734 .justify_content(JustifyContent::CENTER)
3735 .on_click(move || {
3736 if is_enabled {
3737 on_change(match state {
3738 TriState::Checked => TriState::Unchecked,
3739 TriState::Indeterminate => TriState::Checked,
3740 TriState::Unchecked => TriState::Checked,
3741 })
3742 }
3743 })
3744 .then(config.modifier))
3745 .child(
3746 Box(Modifier::new()
3747 .size(sz, sz)
3748 .background(fill)
3749 .border(bd_w, bd, CheckboxDefaults::CORNER_RADIUS)
3750 .clip_rounded(CheckboxDefaults::CORNER_RADIUS)
3751 .align_items(AlignItems::CENTER)
3752 .justify_content(JustifyContent::CENTER))
3753 .child(if symbol_alpha > 0.01 {
3754 Box(Modifier::new().alpha(symbol_alpha)).child(if is_indeterminate {
3755 Box(Modifier::new()
3757 .width(10.0)
3758 .height(2.0)
3759 .background(symbol_col)
3760 .clip_rounded(1.0))
3761 } else {
3762 Icon(Symbol::new("done", '\u{E876}'))
3763 .color(symbol_col)
3764 .size(CheckboxDefaults::CHECK_ICON_SIZE)
3765 })
3766 } else {
3767 Box(Modifier::new())
3768 }),
3769 )
3770}
3771
3772#[derive(Clone, Debug)]
3774pub struct RadioButtonConfig {
3775 pub modifier: Modifier,
3776 pub enabled: bool,
3778 pub selected_color: Color,
3779 pub unselected_color: Color,
3780 pub disabled_selected_color: Color,
3781 pub disabled_unselected_color: Color,
3782 pub state_colors: StateColors,
3783 pub interaction_source: Option<MutableInteractionSource>,
3784}
3785
3786impl Default for RadioButtonConfig {
3787 fn default() -> Self {
3788 Self {
3789 modifier: Modifier::new(),
3790 enabled: true,
3791 selected_color: RadioButtonDefaults::selected_color(),
3792 unselected_color: RadioButtonDefaults::unselected_color(),
3793 disabled_selected_color: RadioButtonDefaults::disabled_selected_color(),
3794 disabled_unselected_color: RadioButtonDefaults::disabled_unselected_color(),
3795 state_colors: RadioButtonDefaults::state_colors_default(),
3796 interaction_source: None,
3797 }
3798 }
3799}
3800
3801static RADIO_COUNTER: AtomicU64 = AtomicU64::new(0);
3805pub fn RadioButton(
3806 selected: bool,
3807 on_select: impl Fn() + 'static,
3808 config: RadioButtonConfig,
3809) -> View {
3810 let th = theme();
3811 let d = RadioButtonDefaults::OUTER_RADIUS * 2.0;
3812
3813 let id = remember(|| RADIO_COUNTER.fetch_add(1, Ordering::Relaxed));
3814 let color_spec = th.motion.color_fast;
3815 let spring = th.motion.spring;
3816
3817 let ring_col = animate_color(
3818 format!("rb_ring_{}", id),
3819 if !config.enabled {
3820 if selected {
3821 config.disabled_selected_color
3822 } else {
3823 config.disabled_unselected_color
3824 }
3825 } else if selected {
3826 config.selected_color
3827 } else {
3828 config.unselected_color
3829 },
3830 color_spec,
3831 );
3832 let dot_size = animate_f32(
3833 format!("rb_dot_{}", id),
3834 if selected {
3835 RadioButtonDefaults::DOT_RADIUS * 2.0
3836 } else {
3837 0.0
3838 },
3839 spring,
3840 );
3841 let dot_col = if !config.enabled {
3842 config.disabled_selected_color
3843 } else {
3844 config.selected_color
3845 };
3846
3847 let cb = move || {
3848 if config.enabled {
3849 on_select()
3850 }
3851 };
3852
3853 let rb_source: Rc<MutableInteractionSource> = config
3854 .interaction_source
3855 .clone()
3856 .map(Rc::new)
3857 .unwrap_or_else(|| remember(MutableInteractionSource::new));
3858 Box(Modifier::new()
3859 .width(RadioButtonDefaults::TOUCH_TARGET_SIZE)
3860 .height(RadioButtonDefaults::TOUCH_TARGET_SIZE)
3861 .padding(0.0)
3862 .clip_rounded(20.0)
3863 .background(Color::TRANSPARENT)
3864 .state_colors(config.state_colors)
3865 .interaction_source(&*rb_source)
3866 .clickable()
3867 .align_items(AlignItems::CENTER)
3868 .justify_content(JustifyContent::CENTER)
3869 .on_click(cb)
3870 .then(config.modifier))
3871 .child(
3872 Box(Modifier::new()
3873 .size(d, d)
3874 .border(RadioButtonDefaults::STROKE_WIDTH, ring_col, d * 0.5)
3875 .clip_rounded(d * 0.5)
3876 .align_items(AlignItems::CENTER)
3877 .justify_content(JustifyContent::CENTER))
3878 .child(if dot_size > 0.5 {
3879 Box(Modifier::new()
3880 .size(dot_size, dot_size)
3881 .background(dot_col)
3882 .clip_rounded(dot_size * 0.5))
3883 } else {
3884 Box(Modifier::new())
3885 }),
3886 )
3887}
3888
3889#[derive(Clone, Debug)]
3891pub struct SwitchConfig {
3892 pub modifier: Modifier,
3893 pub enabled: bool,
3895 pub checked_track_color: Color,
3896 pub unchecked_track_color: Color,
3897 pub checked_thumb_color: Color,
3898 pub unchecked_thumb_color: Color,
3899 pub checked_icon_color: Color,
3901 pub unchecked_icon_color: Color,
3903 pub checked_border_color: Color,
3905 pub unchecked_border_color: Color,
3907 pub disabled_checked_thumb_color: Color,
3908 pub disabled_checked_track_color: Color,
3909 pub disabled_checked_border_color: Color,
3910 pub disabled_checked_icon_color: Color,
3911 pub disabled_unchecked_thumb_color: Color,
3912 pub disabled_unchecked_track_color: Color,
3913 pub disabled_unchecked_border_color: Color,
3914 pub disabled_unchecked_icon_color: Color,
3915 pub state_colors: StateColors,
3916 pub thumb_content: Option<View>,
3917 pub interaction_source: Option<MutableInteractionSource>,
3918}
3919
3920impl Default for SwitchConfig {
3921 fn default() -> Self {
3922 Self {
3923 modifier: Modifier::new(),
3924 enabled: true,
3925 checked_track_color: SwitchDefaults::checked_track_color(),
3926 unchecked_track_color: SwitchDefaults::unchecked_track_color(),
3927 checked_thumb_color: SwitchDefaults::checked_thumb_color(),
3928 unchecked_thumb_color: SwitchDefaults::unchecked_thumb_color(),
3929 checked_icon_color: SwitchDefaults::checked_icon_color(),
3930 unchecked_icon_color: SwitchDefaults::unchecked_icon_color(),
3931 checked_border_color: Color::TRANSPARENT,
3932 unchecked_border_color: SwitchDefaults::unchecked_border_color(),
3933 disabled_checked_thumb_color: SwitchDefaults::disabled_checked_thumb_color(),
3934 disabled_checked_track_color: SwitchDefaults::disabled_checked_track_color(),
3935 disabled_checked_border_color: Color::TRANSPARENT,
3936 disabled_checked_icon_color: SwitchDefaults::disabled_checked_icon_color(),
3937 disabled_unchecked_thumb_color: SwitchDefaults::disabled_unchecked_thumb_color(),
3938 disabled_unchecked_track_color: SwitchDefaults::disabled_unchecked_track_color(),
3939 disabled_unchecked_border_color: SwitchDefaults::disabled_unchecked_border_color(),
3940 disabled_unchecked_icon_color: SwitchDefaults::disabled_unchecked_icon_color(),
3941 state_colors: SwitchDefaults::state_colors_default(),
3942 thumb_content: None,
3943 interaction_source: None,
3944 }
3945 }
3946}
3947
3948static SWITCH_COUNTER: AtomicU64 = AtomicU64::new(0);
3952pub fn Switch(checked: bool, on_change: impl Fn(bool) + 'static, config: SwitchConfig) -> View {
3953 let th = theme();
3954 let track_w = SwitchDefaults::TRACK_WIDTH;
3955 let track_h = SwitchDefaults::TRACK_HEIGHT;
3956
3957 let id = remember(|| SWITCH_COUNTER.fetch_add(1, Ordering::Relaxed));
3958
3959 let hovered = remember(|| Signal::new(false));
3960 let pressed = remember(|| Signal::new(false));
3961
3962 let thumb_target_pos = if checked {
3964 track_w - SwitchDefaults::THUMB_CHECKED_SIZE - 4.0
3965 } else {
3966 8.0
3967 };
3968 let thumb_target_d = if checked {
3969 SwitchDefaults::THUMB_CHECKED_SIZE
3970 } else {
3971 SwitchDefaults::THUMB_UNCHECKED_SIZE
3972 };
3973 let spring = th.motion.spring;
3974
3975 let thumb_left = animate_f32(format!("sw_pos_{}", id), thumb_target_pos, spring);
3976 let thumb_d = animate_f32(format!("sw_d_{}", id), thumb_target_d, spring);
3977 let thumb_top = (track_h - thumb_d) * 0.5;
3978
3979 let color_spec = th.motion.color_fast;
3980 let is_enabled = config.enabled;
3981
3982 let track_bg = animate_color(
3983 format!("sw_tbg_{}", id),
3984 if !is_enabled {
3985 if checked {
3986 config.disabled_checked_track_color
3987 } else {
3988 config.disabled_unchecked_track_color
3989 }
3990 } else if checked {
3991 config.checked_track_color
3992 } else {
3993 config.unchecked_track_color
3994 },
3995 color_spec,
3996 );
3997 let thumb_bg = animate_color(
3998 format!("sw_tmbg_{}", id),
3999 if !is_enabled {
4000 if checked {
4001 config.disabled_checked_thumb_color
4002 } else {
4003 config.disabled_unchecked_thumb_color
4004 }
4005 } else if checked {
4006 config.checked_thumb_color
4007 } else {
4008 config.unchecked_thumb_color
4009 },
4010 color_spec,
4011 );
4012 let track_border = animate_f32(
4013 format!("sw_tb_{}", id),
4014 if !is_enabled {
4015 if checked { 0.0 } else { 2.0 }
4016 } else if checked {
4017 0.0
4018 } else {
4019 2.0
4020 },
4021 color_spec,
4022 );
4023 let border_color = animate_color(
4024 format!("sw_bc_{}", id),
4025 if !is_enabled {
4026 if checked {
4027 config.disabled_checked_border_color
4028 } else {
4029 config.disabled_unchecked_border_color
4030 }
4031 } else if checked {
4032 config.checked_border_color
4033 } else {
4034 config.unchecked_border_color
4035 },
4036 color_spec,
4037 );
4038
4039 let state_overlay = animate_color(
4040 format!("sw_ol_{}", id),
4041 if !is_enabled {
4042 Color::TRANSPARENT
4043 } else if pressed.get() {
4044 config.state_colors.pressed
4045 } else if hovered.get() {
4046 config.state_colors.hovered
4047 } else {
4048 config.state_colors.default
4049 },
4050 color_spec,
4051 );
4052
4053 let sw_source: Rc<MutableInteractionSource> = config
4054 .interaction_source
4055 .clone()
4056 .map(Rc::new)
4057 .unwrap_or_else(|| remember(MutableInteractionSource::new));
4058 Box(Modifier::new()
4059 .size(track_w, track_h)
4060 .padding(0.0)
4061 .clip_rounded(track_h * 0.5)
4062 .background(track_bg)
4063 .border(track_border, border_color, track_h * 0.5)
4064 .interaction_source(&*sw_source)
4065 .clickable()
4066 .on_pointer_enter({
4067 let h = hovered.clone();
4068 move |_| h.set(true)
4069 })
4070 .on_pointer_leave({
4071 let h = hovered.clone();
4072 let p = pressed.clone();
4073 move |_| {
4074 h.set(false);
4075 p.set(false);
4076 }
4077 })
4078 .on_pointer_down({
4079 let p = pressed.clone();
4080 move |_| p.set(true)
4081 })
4082 .on_click({
4083 let cb = on_change;
4084 move || cb(!checked)
4085 })
4086 .on_pointer_up({
4087 let p = pressed.clone();
4088 move |_| p.set(false)
4089 })
4090 .then(config.modifier))
4091 .child((
4092 Box(Modifier::new()
4093 .size(thumb_d, thumb_d)
4094 .background(thumb_bg)
4095 .clip_rounded(thumb_d * 0.5)
4096 .hit_passthrough()
4097 .absolute()
4098 .offset(Some(thumb_left), Some(thumb_top), None, None)),
4099 Box(Modifier::new()
4100 .size(40.0, 40.0)
4101 .clip_rounded(20.0)
4102 .background(state_overlay)
4103 .hit_passthrough()
4104 .absolute()
4105 .offset(
4106 Some(thumb_left + thumb_d * 0.5 - 20.0),
4107 Some(track_h * 0.5 - 20.0),
4108 None,
4109 None,
4110 )),
4111 ))
4112}
4113
4114#[derive(Clone)]
4116pub struct SliderConfig {
4117 pub modifier: Modifier,
4119 pub enabled: bool,
4121 pub active_track_color: Color,
4122 pub inactive_track_color: Color,
4123 pub thumb_color: Color,
4124 pub active_tick_color: Color,
4125 pub inactive_tick_color: Color,
4126 pub disabled_thumb_color: Color,
4127 pub disabled_active_track_color: Color,
4128 pub disabled_inactive_track_color: Color,
4129 pub disabled_active_tick_color: Color,
4130 pub disabled_inactive_tick_color: Color,
4131 pub state_colors: StateColors,
4132 pub on_value_change_finished: Option<Rc<dyn Fn()>>,
4133 pub interaction_source: Option<MutableInteractionSource>,
4134}
4135
4136impl std::fmt::Debug for SliderConfig {
4137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4138 f.debug_struct("SliderConfig")
4139 .field("modifier", &self.modifier)
4140 .field("enabled", &self.enabled)
4141 .field("active_track_color", &self.active_track_color)
4142 .field("inactive_track_color", &self.inactive_track_color)
4143 .field("thumb_color", &self.thumb_color)
4144 .field("active_tick_color", &self.active_tick_color)
4145 .field("inactive_tick_color", &self.inactive_tick_color)
4146 .field("disabled_thumb_color", &self.disabled_thumb_color)
4147 .field(
4148 "disabled_active_track_color",
4149 &self.disabled_active_track_color,
4150 )
4151 .field(
4152 "disabled_inactive_track_color",
4153 &self.disabled_inactive_track_color,
4154 )
4155 .field(
4156 "disabled_active_tick_color",
4157 &self.disabled_active_tick_color,
4158 )
4159 .field(
4160 "disabled_inactive_tick_color",
4161 &self.disabled_inactive_tick_color,
4162 )
4163 .field("state_colors", &self.state_colors)
4164 .field(
4165 "on_value_change_finished",
4166 &self.on_value_change_finished.as_ref().map(|_| ".."),
4167 )
4168 .field(
4169 "interaction_source",
4170 &self.interaction_source.as_ref().map(|_| ".."),
4171 )
4172 .finish()
4173 }
4174}
4175
4176impl Default for SliderConfig {
4177 fn default() -> Self {
4178 Self {
4179 modifier: Modifier::new(),
4180 enabled: true,
4181 active_track_color: SliderDefaults::active_track_color(),
4182 inactive_track_color: SliderDefaults::inactive_track_color(),
4183 thumb_color: SliderDefaults::thumb_color(),
4184 active_tick_color: SliderDefaults::active_tick_color(),
4185 inactive_tick_color: SliderDefaults::inactive_tick_color(),
4186 disabled_thumb_color: SliderDefaults::disabled_thumb_color(),
4187 disabled_active_track_color: SliderDefaults::disabled_active_track_color(),
4188 disabled_inactive_track_color: SliderDefaults::disabled_inactive_track_color(),
4189 disabled_active_tick_color: SliderDefaults::disabled_active_tick_color(),
4190 disabled_inactive_tick_color: SliderDefaults::disabled_inactive_tick_color(),
4191 state_colors: SliderDefaults::state_colors_default(),
4192 on_value_change_finished: None,
4193 interaction_source: None,
4194 }
4195 }
4196}
4197
4198static SLIDER_COUNTER: AtomicU64 = AtomicU64::new(0);
4199
4200fn snap_step(v: f32, min: f32, max: f32, step: Option<f32>) -> f32 {
4201 let v = v.clamp(min, max);
4202 if let Some(s) = step.filter(|s| *s > 0.0) {
4203 let t = ((v - min) / s).round();
4204 (min + t * s).clamp(min, max)
4205 } else {
4206 v
4207 }
4208}
4209
4210fn value_from_x(x: f32, rect: Rect, min: f32, max: f32, step: Option<f32>) -> f32 {
4211 let w = rect.w.max(1.0);
4212 let t = ((x - rect.x) / w).clamp(0.0, 1.0);
4213 let v = min + t * (max - min);
4214 snap_step(v, min, max, step)
4215}
4216
4217pub fn Slider(
4218 value: f32,
4219 range: (f32, f32),
4220 step: Option<f32>,
4221 on_change: impl Fn(f32) + 'static,
4222 config: SliderConfig,
4223) -> View {
4224 assert!(range.0 <= range.1, "Slider range start must be <= end");
4225 if let Some(s) = step {
4226 assert!(s > 0.0, "Slider step must be positive");
4227 }
4228 let id = *remember(|| SLIDER_COUNTER.fetch_add(1, Ordering::Relaxed));
4229 let track_rect = remember_state_with_key(format!("ms_rect_{}", id), || Rect::default());
4230 let drag_active = remember_mutable_with_key(format!("ms_da_{}", id), || false);
4231 let hovered = remember(|| Signal::new(false));
4232
4233 let track_rect_p = track_rect.clone();
4234 let drag_active_p = drag_active.clone();
4235 let hovered_sig = hovered.clone();
4236 let sc = config.state_colors;
4237
4238 let min = range.0;
4239 let max = range.1;
4240 let oc = Rc::new(on_change);
4241 let range_size = (max - min).max(1e-6);
4242 let t = ((value - min) / range_size).clamp(0.0, 1.0);
4243
4244 let tick_frac: Vec<f32> = if let Some(s) = step {
4245 let n = ((max - min) / s.max(1e-6)).round() as usize;
4246 (0..=n).map(|i| i as f32 / n as f32).collect()
4247 } else {
4248 Vec::new()
4249 };
4250
4251 let sl_source: Rc<MutableInteractionSource> = config
4252 .interaction_source
4253 .clone()
4254 .map(Rc::new)
4255 .unwrap_or_else(|| remember(MutableInteractionSource::new));
4256 Box(Modifier::new()
4257 .min_width(200.0)
4258 .height(44.0)
4259 .interaction_source(&*sl_source)
4260 .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
4261 let mul_c = |c: Color| {
4262 Color(
4263 c.0,
4264 c.1,
4265 c.2,
4266 ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
4267 )
4268 };
4269 let track_h = dp_to_px(16.0);
4270 let thumb_w = dp_to_px(4.0);
4271 let thumb_h = dp_to_px(44.0);
4272 let dot_r = dp_to_px(2.0);
4273 let corner = track_h * 0.5;
4274 let gap = thumb_w * 0.5 + dp_to_px(ProgressIndicatorDefaults::SLIDER_THUMB_TRACK_GAP);
4275 let pad = thumb_w * 0.5;
4276 let track_x = rect.x + pad;
4277 let track_w = (rect.w - thumb_w).max(0.0);
4278 let cy = rect.y + rect.h * 0.5;
4279
4280 let kx = if step.is_some() && !tick_frac.is_empty() {
4281 let is_first = (t - tick_frac[0]).abs() < 1e-6;
4282 let is_last = (t - tick_frac[tick_frac.len() - 1]).abs() < 1e-6;
4283 if is_first || is_last {
4284 track_x + t * track_w
4285 } else {
4286 track_x + (track_w - track_h) * t + corner
4287 }
4288 } else {
4289 track_x + t * track_w
4290 };
4291
4292 *track_rect_p.borrow_mut() = Rect {
4293 x: track_x,
4294 y: rect.y,
4295 w: track_w,
4296 h: rect.h,
4297 };
4298
4299 let inactive_x = track_x.max(kx + gap);
4300 let inactive_w = (track_x + track_w - inactive_x).max(0.0);
4301 if inactive_w > 0.0 {
4302 scene.nodes.push(SceneNode::Rect {
4303 rect: Rect {
4304 x: inactive_x,
4305 y: cy - track_h * 0.5,
4306 w: inactive_w,
4307 h: track_h,
4308 },
4309 brush: Brush::Solid(mul_c(config.inactive_track_color)),
4310 radius: [corner; 4],
4311 });
4312 let sx = track_x + track_w - corner;
4313 scene.nodes.push(SceneNode::Ellipse {
4314 rect: Rect {
4315 x: sx - dot_r,
4316 y: cy - dot_r,
4317 w: dot_r * 2.0,
4318 h: dot_r * 2.0,
4319 },
4320 brush: Brush::Solid(mul_c(config.inactive_tick_color)),
4321 });
4322 }
4323 let fill_w = (kx - gap - track_x).max(0.0);
4324 if fill_w > 0.0 {
4325 scene.nodes.push(SceneNode::Rect {
4326 rect: Rect {
4327 x: track_x,
4328 y: cy - track_h * 0.5,
4329 w: fill_w,
4330 h: track_h,
4331 },
4332 brush: Brush::Solid(mul_c(config.active_track_color)),
4333 radius: [corner; 4],
4334 });
4335 }
4336 let tick_start = track_x + corner;
4337 let tick_end = track_x + track_w - corner;
4338 for (i, &tf) in tick_frac.iter().enumerate() {
4339 let tx = tick_start + tf * (tick_end - tick_start);
4340 if i == tick_frac.len() - 1 {
4342 continue;
4343 }
4344 if tx >= kx - gap && tx <= kx + gap {
4345 continue;
4346 }
4347 let on_active = tx <= kx - gap;
4348 scene.nodes.push(SceneNode::Ellipse {
4349 rect: Rect {
4350 x: tx - dot_r,
4351 y: cy - dot_r,
4352 w: dot_r * 2.0,
4353 h: dot_r * 2.0,
4354 },
4355 brush: Brush::Solid(mul_c(if on_active {
4356 config.active_tick_color
4357 } else {
4358 config.inactive_tick_color
4359 })),
4360 });
4361 }
4362 let da = *drag_active_p.get();
4363 let hv = hovered_sig.get();
4364 let tw = if da { thumb_w * 0.5 } else { thumb_w };
4365 scene.nodes.push(SceneNode::Rect {
4366 rect: Rect {
4367 x: kx - tw * 0.5,
4368 y: cy - thumb_h * 0.5,
4369 w: tw,
4370 h: thumb_h,
4371 },
4372 brush: Brush::Solid(mul_c(config.thumb_color)),
4373 radius: [tw * 0.5; 4],
4374 });
4375 let sc_target = if da {
4376 sc.pressed
4377 } else if hv {
4378 sc.hovered
4379 } else {
4380 sc.default
4381 };
4382 if sc_target.3 > 0 {
4383 scene.nodes.push(SceneNode::Rect {
4384 rect: Rect {
4385 x: kx - tw * 0.5,
4386 y: cy - thumb_h * 0.5,
4387 w: tw,
4388 h: thumb_h,
4389 },
4390 brush: Brush::Solid(mul_c(sc_target)),
4391 radius: [tw * 0.5; 4],
4392 });
4393 }
4394 })
4395 .on_pointer_enter({
4396 let h = hovered.clone();
4397 move |_pe: PointerEvent| h.set(true)
4398 })
4399 .on_pointer_leave({
4400 let h = hovered.clone();
4401 move |_pe: PointerEvent| h.set(false)
4402 })
4403 .on_pointer_down({
4404 let oc = oc.clone();
4405 let track_rect = track_rect.clone();
4406 let drag_active = drag_active.clone();
4407 move |pe: PointerEvent| {
4408 drag_active.set(true);
4409 let r = *track_rect.borrow();
4410 (oc)(value_from_x(pe.position.x, r, min, max, step));
4411 }
4412 })
4413 .on_pointer_move({
4414 let oc = oc.clone();
4415 let track_rect = track_rect.clone();
4416 let drag_active = drag_active.clone();
4417 move |pe: PointerEvent| {
4418 if !*drag_active.get() {
4419 return;
4420 }
4421 let r = *track_rect.borrow();
4422 (oc)(value_from_x(pe.position.x, r, min, max, step));
4423 }
4424 })
4425 .on_pointer_up({
4426 let on_finished = config.on_value_change_finished.clone();
4427 move |_pe: PointerEvent| {
4428 drag_active.set(false);
4429 if let Some(ref cb) = on_finished {
4430 (cb)();
4431 }
4432 }
4433 })
4434 .on_scroll({
4435 let oc = oc.clone();
4436 move |d: Vec2| -> Vec2 {
4437 let dir = if d.y < -0.5 {
4438 1
4439 } else if d.y > 0.5 {
4440 -1
4441 } else {
4442 0
4443 };
4444 if dir == 0 {
4445 return d;
4446 }
4447 let step_val = step.unwrap_or(1.0).max(1e-6);
4448 let new_val = snap_step(value + (dir as f32) * step_val, min, max, step);
4449 if (new_val - value).abs() > 1e-6 {
4450 (oc)(new_val);
4451 Vec2 { x: d.x, y: 0.0 }
4452 } else {
4453 d
4454 }
4455 }
4456 })
4457 .then(config.modifier))
4458 .semantics(Semantics {
4459 role: Role::Slider,
4460 label: None,
4461 focused: false,
4462 enabled: true,
4463 selectable_group: false,
4464 })
4465}
4466
4467pub fn RangeSlider(
4468 start: f32,
4469 end: f32,
4470 range: (f32, f32),
4471 step: Option<f32>,
4472 on_change: impl Fn(f32, f32) + 'static,
4473 config: SliderConfig,
4474) -> View {
4475 assert!(range.0 <= range.1, "Slider range start must be <= end");
4476 if let Some(s) = step {
4477 assert!(s > 0.0, "Slider step must be positive");
4478 }
4479 let id = *remember(|| SLIDER_COUNTER.fetch_add(1, Ordering::Relaxed));
4480 let track_rect = remember_state_with_key(format!("mrs_rect_{}", id), || Rect::default());
4481 let drag_active = remember_mutable_with_key(format!("mrs_da_{}", id), || false);
4482 let active_thumb = remember_mutable_with_key(format!("mrs_at_{}", id), || false);
4483 let hovered = remember(|| Signal::new(false));
4484
4485 let min = range.0;
4486 let max = range.1;
4487 let oc = Rc::new(on_change);
4488 let range_size = (max - min).max(1e-6);
4489 let t0 = ((start - min) / range_size).clamp(0.0, 1.0);
4490 let t1 = ((end - min) / range_size).clamp(0.0, 1.0);
4491 let sc = config.state_colors;
4492 let is_enabled = config.enabled;
4493
4494 let act_trk = if !is_enabled {
4495 config.disabled_active_track_color
4496 } else {
4497 config.active_track_color
4498 };
4499 let inact_trk = if !is_enabled {
4500 config.disabled_inactive_track_color
4501 } else {
4502 config.inactive_track_color
4503 };
4504 let act_tick = if !is_enabled {
4505 config.disabled_active_tick_color
4506 } else {
4507 config.active_tick_color
4508 };
4509 let inact_tick = if !is_enabled {
4510 config.disabled_inactive_tick_color
4511 } else {
4512 config.inactive_tick_color
4513 };
4514 let thumb_col = if !is_enabled {
4515 config.disabled_thumb_color
4516 } else {
4517 config.thumb_color
4518 };
4519
4520 let tick_frac: Vec<f32> = if let Some(s) = step {
4521 let n = ((max - min) / s.max(1e-6)).round() as usize;
4522 (0..=n).map(|i| i as f32 / n as f32).collect()
4523 } else {
4524 Vec::new()
4525 };
4526
4527 let track_rect_p = track_rect.clone();
4528 let drag_active_p = drag_active.clone();
4529 let active_thumb_p = active_thumb.clone();
4530 let hovered_sig = hovered.clone();
4531
4532 Box(Modifier::new()
4533 .min_width(200.0)
4534 .height(44.0)
4535 .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
4536 let mul_c = |c: Color| {
4537 Color(
4538 c.0,
4539 c.1,
4540 c.2,
4541 ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
4542 )
4543 };
4544 let track_h = dp_to_px(16.0);
4545 let thumb_w = dp_to_px(4.0);
4546 let thumb_h = dp_to_px(44.0);
4547 let dot_r = dp_to_px(2.0);
4548 let corner = track_h * 0.5;
4549 let gap = thumb_w * 0.5 + dp_to_px(ProgressIndicatorDefaults::SLIDER_THUMB_TRACK_GAP);
4550 let pad = thumb_w * 0.5;
4551 let track_x = rect.x + pad;
4552 let track_w = (rect.w - thumb_w).max(0.0);
4553 let cy = rect.y + rect.h * 0.5;
4554
4555 let thumb_pos = |tf: f32, fracs: &[f32]| {
4556 if step.is_some() && !fracs.is_empty() {
4557 let is_first = (tf - fracs[0]).abs() < 1e-6;
4558 let is_last = (tf - fracs[fracs.len() - 1]).abs() < 1e-6;
4559 if is_first || is_last {
4560 track_x + tf * track_w
4561 } else {
4562 track_x + (track_w - track_h) * tf + corner
4563 }
4564 } else {
4565 track_x + tf * track_w
4566 }
4567 };
4568 let k0 = thumb_pos(t0, &tick_frac);
4569 let k1 = thumb_pos(t1, &tick_frac);
4570 let active_l = k0.min(k1);
4571 let active_r = k0.max(k1);
4572
4573 *track_rect_p.borrow_mut() = Rect {
4574 x: track_x,
4575 y: rect.y,
4576 w: track_w,
4577 h: rect.h,
4578 };
4579
4580 let linactive_w = (active_l - gap - track_x).max(0.0);
4581 if linactive_w > 0.0 {
4582 scene.nodes.push(SceneNode::Rect {
4583 rect: Rect {
4584 x: track_x,
4585 y: cy - track_h * 0.5,
4586 w: linactive_w,
4587 h: track_h,
4588 },
4589 brush: Brush::Solid(mul_c(inact_trk)),
4590 radius: [corner; 4],
4591 });
4592 let sx0 = track_x + corner;
4593 scene.nodes.push(SceneNode::Ellipse {
4594 rect: Rect {
4595 x: sx0 - dot_r,
4596 y: cy - dot_r,
4597 w: dot_r * 2.0,
4598 h: dot_r * 2.0,
4599 },
4600 brush: Brush::Solid(mul_c(inact_tick)),
4601 });
4602 }
4603 let rinactive_x = (active_r + gap).min(track_x + track_w);
4604 let rinactive_w = (track_x + track_w - rinactive_x).max(0.0);
4605 if rinactive_w > 0.0 {
4606 scene.nodes.push(SceneNode::Rect {
4607 rect: Rect {
4608 x: rinactive_x,
4609 y: cy - track_h * 0.5,
4610 w: rinactive_w,
4611 h: track_h,
4612 },
4613 brush: Brush::Solid(mul_c(inact_trk)),
4614 radius: [corner; 4],
4615 });
4616 let sx = track_x + track_w - corner;
4617 scene.nodes.push(SceneNode::Ellipse {
4618 rect: Rect {
4619 x: sx - dot_r,
4620 y: cy - dot_r,
4621 w: dot_r * 2.0,
4622 h: dot_r * 2.0,
4623 },
4624 brush: Brush::Solid(mul_c(inact_tick)),
4625 });
4626 }
4627 let active_w = (active_r - gap - (active_l + gap)).max(0.0);
4628 if active_w > 0.0 {
4629 scene.nodes.push(SceneNode::Rect {
4630 rect: Rect {
4631 x: active_l + gap,
4632 y: cy - track_h * 0.5,
4633 w: active_w,
4634 h: track_h,
4635 },
4636 brush: Brush::Solid(mul_c(act_trk)),
4637 radius: [corner; 4],
4638 });
4639 }
4640 let tick_start = track_x + corner;
4641 let tick_end = track_x + track_w - corner;
4642 for (i, &tf) in tick_frac.iter().enumerate() {
4643 let tx = tick_start + tf * (tick_end - tick_start);
4644 if i == 0 || i == tick_frac.len() - 1 {
4646 continue;
4647 }
4648 let in_lgap = tx >= active_l - gap && tx <= active_l + gap;
4649 let in_rgap = tx >= active_r - gap && tx <= active_r + gap;
4650 if in_lgap || in_rgap {
4651 continue;
4652 }
4653 let on_active = tx >= active_l + gap && tx <= active_r - gap;
4654 scene.nodes.push(SceneNode::Ellipse {
4655 rect: Rect {
4656 x: tx - dot_r,
4657 y: cy - dot_r,
4658 w: dot_r * 2.0,
4659 h: dot_r * 2.0,
4660 },
4661 brush: Brush::Solid(mul_c(if on_active { act_tick } else { inact_tick })),
4662 });
4663 }
4664 let da = *drag_active_p.get();
4665 let at = *active_thumb_p.get();
4666 let hv = hovered_sig.get();
4667 let thumbs = [k0, k1];
4668 for (idx, &kx) in thumbs.iter().enumerate() {
4669 let is_active = da && (if idx == 0 { !at } else { at });
4670 let tw = if is_active { thumb_w * 0.5 } else { thumb_w };
4671 scene.nodes.push(SceneNode::Rect {
4672 rect: Rect {
4673 x: kx - tw * 0.5,
4674 y: cy - thumb_h * 0.5,
4675 w: tw,
4676 h: thumb_h,
4677 },
4678 brush: Brush::Solid(mul_c(thumb_col)),
4679 radius: [tw * 0.5; 4],
4680 });
4681 let sc_target = if !is_enabled {
4682 Color::TRANSPARENT
4683 } else if is_active {
4684 sc.pressed
4685 } else if hv {
4686 sc.hovered
4687 } else {
4688 sc.default
4689 };
4690 if sc_target.3 > 0 {
4691 scene.nodes.push(SceneNode::Rect {
4692 rect: Rect {
4693 x: kx - tw * 0.5,
4694 y: cy - thumb_h * 0.5,
4695 w: tw,
4696 h: thumb_h,
4697 },
4698 brush: Brush::Solid(mul_c(sc_target)),
4699 radius: [tw * 0.5; 4],
4700 });
4701 }
4702 }
4703 })
4704 .on_pointer_enter({
4705 let h = hovered.clone();
4706 let en = is_enabled;
4707 move |_pe: PointerEvent| {
4708 if en {
4709 h.set(true);
4710 }
4711 }
4712 })
4713 .on_pointer_leave({
4714 let h = hovered.clone();
4715 move |_pe: PointerEvent| h.set(false)
4716 })
4717 .on_pointer_down({
4718 let oc = oc.clone();
4719 let track_rect = track_rect.clone();
4720 let drag_active = drag_active.clone();
4721 let active_thumb = active_thumb.clone();
4722 let en = is_enabled;
4723 move |pe: PointerEvent| {
4724 if !en {
4725 return;
4726 }
4727 drag_active.set(true);
4728 let r = *track_rect.borrow();
4729 let v = value_from_x(pe.position.x, r, min, max, step);
4730 let use_end = (v - end).abs() < (v - start).abs();
4731 active_thumb.set(use_end);
4732 let (a, b) = if use_end {
4733 (start, v.max(start))
4734 } else {
4735 (v.min(end), end)
4736 };
4737 (oc)(a, b);
4738 }
4739 })
4740 .on_pointer_move({
4741 let oc = oc.clone();
4742 let track_rect = track_rect.clone();
4743 let drag_active = drag_active.clone();
4744 let active_thumb = active_thumb.clone();
4745 move |pe: PointerEvent| {
4746 if !*drag_active.get() {
4747 return;
4748 }
4749 let r = *track_rect.borrow();
4750 let v = value_from_x(pe.position.x, r, min, max, step);
4751 let use_end = *active_thumb.get();
4752 let (a, b) = if use_end {
4753 (start, v.max(start))
4754 } else {
4755 (v.min(end), end)
4756 };
4757 (oc)(a, b);
4758 }
4759 })
4760 .on_pointer_up({
4761 let drag_active = drag_active.clone();
4762 let active_thumb = active_thumb.clone();
4763 move |_pe: PointerEvent| {
4764 drag_active.set(false);
4765 active_thumb.set(false);
4766 }
4767 })
4768 .on_scroll({
4769 let oc = oc.clone();
4770 let active_thumb = active_thumb.clone();
4771 let en = is_enabled;
4772 move |d: Vec2| -> Vec2 {
4773 if !en {
4774 return d;
4775 }
4776 let dir = if d.y < -0.5 {
4777 1
4778 } else if d.y > 0.5 {
4779 -1
4780 } else {
4781 0
4782 };
4783 if dir == 0 {
4784 return d;
4785 }
4786 let step_val = step.unwrap_or(1.0).max(1e-6);
4787 let use_end = *active_thumb.get();
4788 let (mut a, mut b) = (start, end);
4789 if use_end {
4790 b = snap_step(end + (dir as f32) * step_val, min, max, step).max(a);
4791 } else {
4792 a = snap_step(start + (dir as f32) * step_val, min, max, step).min(b);
4793 }
4794 if (a - start).abs() > 1e-6 || (b - end).abs() > 1e-6 {
4795 (oc)(a, b);
4796 Vec2 { x: d.x, y: 0.0 }
4797 } else {
4798 d
4799 }
4800 }
4801 })
4802 .then(config.modifier))
4803 .semantics(Semantics {
4804 role: Role::Slider,
4805 label: None,
4806 focused: false,
4807 enabled: is_enabled,
4808 selectable_group: false,
4809 })
4810}
4811
4812#[derive(Clone, Debug)]
4814pub struct CardConfig {
4815 pub modifier: Modifier,
4816 pub enabled: bool,
4818 pub container_color: Color,
4819 pub content_color: Color,
4820 pub disabled_container_color: Color,
4821 pub disabled_content_color: Color,
4822 pub shape_radius: f32,
4823 pub tonal_elevation: f32,
4824 pub state_elevation: Option<StateElevation>,
4825 pub border: Option<(f32, Color)>,
4826 pub interaction_source: Option<MutableInteractionSource>,
4827}
4828
4829impl Default for CardConfig {
4830 fn default() -> Self {
4831 Self {
4832 modifier: Modifier::new(),
4833 enabled: true,
4834 container_color: CardDefaults::filled_container_color(),
4835 content_color: CardDefaults::filled_content_color(),
4836 disabled_container_color: CardDefaults::disabled_container_color(),
4837 disabled_content_color: CardDefaults::disabled_content_color(),
4838 shape_radius: CardDefaults::SHAPE_RADIUS,
4839 tonal_elevation: CardDefaults::ELEVATION,
4840 state_elevation: None,
4841 border: None,
4842 interaction_source: None,
4843 }
4844 }
4845}
4846
4847pub fn Card(config: CardConfig, content: impl FnOnce() -> View) -> View {
4849 let bg = if !config.enabled {
4850 config.disabled_container_color
4851 } else {
4852 config.container_color
4853 };
4854 let fg = if !config.enabled {
4855 config.disabled_content_color
4856 } else {
4857 config.content_color
4858 };
4859 let source: Rc<MutableInteractionSource> = config
4860 .interaction_source
4861 .clone()
4862 .map(Rc::new)
4863 .unwrap_or_else(|| remember(MutableInteractionSource::new));
4864 let mut m = Modifier::new()
4865 .background(bg)
4866 .clip_rounded(config.shape_radius)
4867 .interaction_source(&*source)
4868 .then(config.modifier);
4869 if let Some((w, c)) = config.border {
4870 m = m.border(w, c, config.shape_radius);
4871 }
4872 if let Some(se) = config.state_elevation {
4873 m = m.state_elevation(se);
4874 } else if config.tonal_elevation > 0.0 {
4875 m = m.state_elevation(StateElevation {
4876 default: config.tonal_elevation,
4877 hovered: config.tonal_elevation,
4878 pressed: config.tonal_elevation,
4879 disabled: 0.0,
4880 });
4881 }
4882 Box(m).color(fg).child(content())
4883}
4884
4885pub fn ElevatedCard(config: CardConfig, content: impl FnOnce() -> View) -> View {
4887 let th = theme();
4888 Card(
4889 CardConfig {
4890 container_color: CardDefaults::elevated_container_color(),
4891 state_elevation: Some(StateElevation {
4892 default: th.elevation.level1,
4893 hovered: th.elevation.level2,
4894 pressed: th.elevation.level3,
4895 disabled: 0.0,
4896 }),
4897 ..config
4898 },
4899 content,
4900 )
4901}
4902
4903pub fn OutlinedCard(config: CardConfig, content: impl FnOnce() -> View) -> View {
4905 Card(
4906 CardConfig {
4907 container_color: CardDefaults::outlined_container_color(),
4908 border: Some((1.0, CardDefaults::outlined_border_color())),
4909 ..config
4910 },
4911 content,
4912 )
4913}
4914
4915fn card_state_colors(bg: Color) -> StateColors {
4916 let th = theme();
4917 StateColors {
4918 default: Color::TRANSPARENT,
4919 hovered: th.on_surface.with_alpha_f32(0.08).composite_over(bg),
4920 pressed: th.on_surface.with_alpha_f32(0.12).composite_over(bg),
4921 disabled: th.on_surface.with_alpha_f32(0.12).composite_over(bg),
4922 }
4923}
4924
4925fn clickable_card_impl(
4926 on_click: impl Fn() + 'static,
4927 modifier: Modifier,
4928 bg: Color,
4929 shape_radius: f32,
4930 config: CardConfig,
4931 content: impl FnOnce() -> View,
4932) -> View {
4933 let m = modifier
4934 .state_colors(card_state_colors(bg))
4935 .clickable()
4936 .on_pointer_down({
4937 let cb = on_click;
4938 let en = config.enabled;
4939 move |_| {
4940 if en {
4941 cb();
4942 }
4943 }
4944 });
4945 Card(
4946 CardConfig {
4947 modifier: m,
4948 enabled: config.enabled,
4949 container_color: bg,
4950 content_color: config.content_color,
4951 disabled_container_color: config.disabled_container_color,
4952 disabled_content_color: config.disabled_content_color,
4953 shape_radius,
4954 border: config.border,
4955 state_elevation: config.state_elevation,
4956 tonal_elevation: config.tonal_elevation,
4957 interaction_source: config.interaction_source.clone(),
4958 },
4959 || Column(Modifier::new().fill_max_size()).child(content()),
4960 )
4961}
4962
4963pub fn ClickableCard(
4965 on_click: impl Fn() + 'static,
4966 modifier: Modifier,
4967 config: CardConfig,
4968 content: impl FnOnce() -> View,
4969) -> View {
4970 let th = theme();
4971 clickable_card_impl(
4972 on_click,
4973 modifier,
4974 th.surface_container_highest,
4975 th.shapes.medium,
4976 config,
4977 content,
4978 )
4979}
4980
4981pub fn ClickableElevatedCard(
4983 on_click: impl Fn() + 'static,
4984 modifier: Modifier,
4985 config: CardConfig,
4986 content: impl FnOnce() -> View,
4987) -> View {
4988 let th = theme();
4989 let cfg = CardConfig {
4990 state_elevation: Some(StateElevation {
4991 default: th.elevation.level1,
4992 hovered: th.elevation.level2,
4993 pressed: th.elevation.level3,
4994 disabled: 0.0,
4995 }),
4996 ..config
4997 };
4998 clickable_card_impl(
4999 on_click,
5000 modifier,
5001 th.surface,
5002 th.shapes.medium,
5003 cfg,
5004 content,
5005 )
5006}
5007
5008pub fn ClickableOutlinedCard(
5010 on_click: impl Fn() + 'static,
5011 modifier: Modifier,
5012 config: CardConfig,
5013 content: impl FnOnce() -> View,
5014) -> View {
5015 let th = theme();
5016 let cfg = CardConfig {
5017 border: Some((1.0, th.outline_variant)),
5018 ..config
5019 };
5020 clickable_card_impl(
5021 on_click,
5022 modifier,
5023 th.surface,
5024 th.shapes.medium,
5025 cfg,
5026 content,
5027 )
5028}
5029
5030#[derive(Clone, Debug)]
5032pub struct SnackbarConfig {
5033 pub modifier: Modifier,
5034 pub container_color: Color,
5035 pub content_color: Color,
5036 pub action_color: Color,
5037 pub dismiss_action_content_color: Color,
5038 pub action_on_new_line: bool,
5039 pub shape_radius: f32,
5040 pub min_height: f32,
5041 pub min_width: f32,
5042 pub max_width: f32,
5043}
5044
5045impl Default for SnackbarConfig {
5046 fn default() -> Self {
5047 Self {
5048 modifier: Modifier::new(),
5049 container_color: SnackbarDefaults::container_color(),
5050 content_color: SnackbarDefaults::content_color(),
5051 action_color: SnackbarDefaults::action_color(),
5052 dismiss_action_content_color: SnackbarDefaults::dismiss_action_content_color(),
5053 action_on_new_line: false,
5054 shape_radius: SnackbarDefaults::SHAPE_RADIUS,
5055 min_height: SnackbarDefaults::MIN_HEIGHT,
5056 min_width: SnackbarDefaults::MIN_WIDTH,
5057 max_width: SnackbarDefaults::MAX_WIDTH,
5058 }
5059 }
5060}
5061
5062#[derive(Clone, Copy, Debug)]
5064pub struct ChipColors {
5065 pub container_color: Color,
5066 pub label_color: Color,
5067 pub leading_icon_color: Color,
5068 pub trailing_icon_color: Color,
5069 pub disabled_container_color: Color,
5070 pub disabled_label_color: Color,
5071 pub disabled_leading_icon_color: Color,
5072 pub disabled_trailing_icon_color: Color,
5073 pub selected_container_color: Color,
5074 pub selected_label_color: Color,
5075 pub selected_leading_icon_color: Color,
5076 pub selected_trailing_icon_color: Color,
5077 pub disabled_selected_container_color: Color,
5078}
5079
5080impl ChipColors {
5081 pub fn container(&self, enabled: bool, selected: bool) -> Color {
5082 match (enabled, selected) {
5083 (true, true) => self.selected_container_color,
5084 (true, false) => self.container_color,
5085 (false, true) => self.disabled_selected_container_color,
5086 (false, false) => self.disabled_container_color,
5087 }
5088 }
5089 pub fn label(&self, enabled: bool, selected: bool) -> Color {
5090 if !enabled {
5091 self.disabled_label_color
5092 } else if selected {
5093 self.selected_label_color
5094 } else {
5095 self.label_color
5096 }
5097 }
5098 pub fn leading_icon(&self, enabled: bool, selected: bool) -> Color {
5099 if !enabled {
5100 self.disabled_leading_icon_color
5101 } else if selected {
5102 self.selected_leading_icon_color
5103 } else {
5104 self.leading_icon_color
5105 }
5106 }
5107 pub fn trailing_icon(&self, enabled: bool, selected: bool) -> Color {
5108 if !enabled {
5109 self.disabled_trailing_icon_color
5110 } else if selected {
5111 self.selected_trailing_icon_color
5112 } else {
5113 self.trailing_icon_color
5114 }
5115 }
5116}
5117
5118#[derive(Clone, Copy, Debug)]
5120pub struct ChipElevation {
5121 pub default: f32,
5122 pub hovered: f32,
5123 pub focused: f32,
5124 pub pressed: f32,
5125 pub dragged: f32,
5126 pub disabled: f32,
5127}
5128
5129impl ChipElevation {
5130 pub fn to_state_elevation(&self) -> StateElevation {
5131 StateElevation {
5132 default: self.default,
5133 hovered: self.hovered,
5134 pressed: self.pressed,
5135 disabled: self.disabled,
5136 }
5137 }
5138}
5139
5140impl Default for ChipElevation {
5141 fn default() -> Self {
5142 Self {
5143 default: ChipDefaults::elevation_default(),
5144 hovered: ChipDefaults::elevation_hovered(),
5145 focused: ChipDefaults::elevation_focused(),
5146 pressed: ChipDefaults::elevation_pressed(),
5147 dragged: ChipDefaults::elevation_dragged(),
5148 disabled: ChipDefaults::elevation_disabled(),
5149 }
5150 }
5151}
5152
5153#[derive(Clone, Debug)]
5155pub struct ChipConfig {
5156 pub modifier: Modifier,
5157 pub enabled: bool,
5158 pub colors: ChipColors,
5159 pub elevation: ChipElevation,
5160 pub border_width: f32,
5161 pub border_color: Color,
5162 pub selected_border_color: Color,
5163 pub disabled_border_color: Color,
5164 pub disabled_selected_border_color: Color,
5165 pub shape_radius: f32,
5166 pub horizontal_padding: f32,
5167 pub interaction_source: Option<MutableInteractionSource>,
5168}
5169
5170impl Default for ChipConfig {
5171 fn default() -> Self {
5172 Self {
5173 modifier: Modifier::new(),
5174 enabled: true,
5175 colors: ChipColors {
5176 container_color: ChipDefaults::container_color(),
5177 label_color: ChipDefaults::label_color(),
5178 leading_icon_color: ChipDefaults::leading_icon_color(),
5179 trailing_icon_color: ChipDefaults::trailing_icon_color(),
5180 disabled_container_color: ChipDefaults::disabled_container_color(),
5181 disabled_label_color: ChipDefaults::disabled_label_color(),
5182 disabled_leading_icon_color: ChipDefaults::disabled_leading_icon_color(),
5183 disabled_trailing_icon_color: ChipDefaults::disabled_trailing_icon_color(),
5184 selected_container_color: ChipDefaults::selected_container_color(),
5185 selected_label_color: ChipDefaults::selected_label_color(),
5186 selected_leading_icon_color: ChipDefaults::selected_leading_icon_color(),
5187 selected_trailing_icon_color: ChipDefaults::selected_trailing_icon_color(),
5188 disabled_selected_container_color: ChipDefaults::disabled_selected_container_color(
5189 ),
5190 },
5191 elevation: ChipElevation::default(),
5192 border_width: ChipDefaults::BORDER_WIDTH,
5193 border_color: ChipDefaults::border_color(),
5194 selected_border_color: ChipDefaults::selected_border_color(),
5195 disabled_border_color: ChipDefaults::disabled_border_color(),
5196 disabled_selected_border_color: ChipDefaults::disabled_selected_border_color(),
5197 shape_radius: ChipDefaults::SHAPE_RADIUS,
5198 horizontal_padding: ChipDefaults::HORIZONTAL_PADDING,
5199 interaction_source: None,
5200 }
5201 }
5202}
5203
5204pub fn AssistChip(
5206 on_click: impl Fn() + 'static,
5207 label: View,
5208 leading_icon: Option<View>,
5209 trailing_icon: Option<View>,
5210 config: ChipConfig,
5211) -> View {
5212 let th = theme();
5213 let is_enabled = config.enabled;
5214 let colors = &config.colors;
5215 let bg = colors.container(is_enabled, false);
5216 let label_color = colors.label(is_enabled, false);
5217 let leading_color = colors.leading_icon(is_enabled, false);
5218 let trailing_color = colors.trailing_icon(is_enabled, false);
5219 let border = if is_enabled {
5220 config.border_color
5221 } else {
5222 config.disabled_border_color
5223 };
5224 let shape = config.shape_radius;
5225 let ch_source: Rc<MutableInteractionSource> = config
5226 .interaction_source
5227 .clone()
5228 .map(Rc::new)
5229 .unwrap_or_else(|| remember(MutableInteractionSource::new));
5230
5231 let mut m = Modifier::new()
5232 .state_colors(StateColors {
5233 default: Color::TRANSPARENT,
5234 hovered: th.on_surface.with_alpha_f32(0.08),
5235 pressed: th.on_surface.with_alpha_f32(0.12),
5236 disabled: Color::TRANSPARENT,
5237 })
5238 .padding_values(PaddingValues {
5239 left: config.horizontal_padding,
5240 right: config.horizontal_padding,
5241 top: 8.0,
5242 bottom: 8.0,
5243 })
5244 .background(bg)
5245 .clip_rounded(shape)
5246 .interaction_source(&*ch_source)
5247 .then(config.modifier);
5248
5249 if config.border_width > 0.0 && border != Color::TRANSPARENT {
5250 m = m.border(config.border_width, border, shape);
5251 }
5252
5253 if is_enabled {
5254 m = m.clickable().on_click(move || on_click());
5255 }
5256
5257 Box(m).child(
5258 Row(Modifier::new().align_items(AlignItems::CENTER)).child((
5259 leading_icon
5260 .map(|v| {
5261 Box(Modifier::new().padding_values(PaddingValues {
5262 left: 0.0,
5263 right: 8.0,
5264 top: 0.0,
5265 bottom: 0.0,
5266 }))
5267 .child(with_content_color(leading_color, move || v))
5268 })
5269 .unwrap_or(Box(Modifier::new())),
5270 with_content_color(label_color, move || label),
5271 trailing_icon
5272 .map(|v| {
5273 Box(Modifier::new().padding_values(PaddingValues {
5274 left: 8.0,
5275 right: 0.0,
5276 top: 0.0,
5277 bottom: 0.0,
5278 }))
5279 .child(with_content_color(trailing_color, move || v))
5280 })
5281 .unwrap_or(Box(Modifier::new())),
5282 )),
5283 )
5284}
5285
5286pub fn ElevatedAssistChip(
5288 on_click: impl Fn() + 'static,
5289 label: View,
5290 leading_icon: Option<View>,
5291 trailing_icon: Option<View>,
5292 config: ChipConfig,
5293) -> View {
5294 let th = theme();
5295 let is_enabled = config.enabled;
5296 let colors = &config.colors;
5297 let bg = colors.container(is_enabled, false);
5298 let label_color = colors.label(is_enabled, false);
5299 let leading_color = colors.leading_icon(is_enabled, false);
5300 let trailing_color = colors.trailing_icon(is_enabled, false);
5301 let shape = config.shape_radius;
5302
5303 let mut m = Modifier::new()
5304 .state_colors(StateColors {
5305 default: Color::TRANSPARENT,
5306 hovered: th.on_surface.with_alpha_f32(0.08),
5307 pressed: th.on_surface.with_alpha_f32(0.12),
5308 disabled: Color::TRANSPARENT,
5309 })
5310 .state_elevation(config.elevation.to_state_elevation())
5311 .padding_values(PaddingValues {
5312 left: config.horizontal_padding,
5313 right: config.horizontal_padding,
5314 top: 8.0,
5315 bottom: 8.0,
5316 })
5317 .background(bg)
5318 .clip_rounded(shape)
5319 .then(config.modifier);
5320
5321 if is_enabled {
5322 m = m.clickable().on_click(move || on_click());
5323 }
5324
5325 Box(m).child(
5326 Row(Modifier::new().align_items(AlignItems::CENTER)).child((
5327 leading_icon
5328 .map(|v| {
5329 Box(Modifier::new().padding_values(PaddingValues {
5330 left: 0.0,
5331 right: 8.0,
5332 top: 0.0,
5333 bottom: 0.0,
5334 }))
5335 .child(with_content_color(leading_color, move || v))
5336 })
5337 .unwrap_or(Box(Modifier::new())),
5338 with_content_color(label_color, move || label),
5339 trailing_icon
5340 .map(|v| {
5341 Box(Modifier::new().padding_values(PaddingValues {
5342 left: 8.0,
5343 right: 0.0,
5344 top: 0.0,
5345 bottom: 0.0,
5346 }))
5347 .child(with_content_color(trailing_color, move || v))
5348 })
5349 .unwrap_or(Box(Modifier::new())),
5350 )),
5351 )
5352}
5353
5354#[derive(Clone, Debug)]
5356pub struct NavigationBarConfig {
5357 pub modifier: Modifier,
5358 pub container_color: Color,
5359 pub content_color: Color,
5360 pub selected_icon_color: Color,
5361 pub selected_text_color: Color,
5362 pub unselected_icon_color: Color,
5363 pub unselected_text_color: Color,
5364 pub indicator_color: Color,
5365 pub height: f32,
5366 pub tonal_elevation: f32,
5367 pub indicator_opacity: f32,
5368 pub indicator_radius: f32,
5369 pub item_spacing: f32,
5370 pub indicator_width: f32,
5371 pub indicator_height: f32,
5372}
5373
5374impl Default for NavigationBarConfig {
5375 fn default() -> Self {
5376 Self {
5377 modifier: Modifier::new(),
5378 container_color: NavigationBarDefaults::container_color(),
5379 content_color: NavigationBarDefaults::content_color(),
5380 selected_icon_color: NavigationBarDefaults::selected_icon_color(),
5381 selected_text_color: NavigationBarDefaults::selected_text_color(),
5382 unselected_icon_color: NavigationBarDefaults::unselected_icon_color(),
5383 unselected_text_color: NavigationBarDefaults::unselected_text_color(),
5384 indicator_color: NavigationBarDefaults::indicator_color(),
5385 height: NavigationBarDefaults::HEIGHT,
5386 tonal_elevation: NavigationBarDefaults::TONAL_ELEVATION,
5387 indicator_opacity: NavigationBarDefaults::ITEM_ACTIVE_INDICATOR_OPACITY,
5388 indicator_radius: NavigationBarDefaults::INDICATOR_RADIUS,
5389 item_spacing: NavigationBarDefaults::ITEM_SPACING,
5390 indicator_width: NavigationBarDefaults::ACTIVE_INDICATOR_WIDTH,
5391 indicator_height: NavigationBarDefaults::ACTIVE_INDICATOR_HEIGHT,
5392 }
5393 }
5394}
5395
5396#[derive(Clone, Debug)]
5398pub struct NavigationRailConfig {
5399 pub modifier: Modifier,
5400 pub container_color: Color,
5401 pub selected_icon_color: Color,
5402 pub selected_text_color: Color,
5403 pub unselected_icon_color: Color,
5404 pub unselected_text_color: Color,
5405 pub indicator_color: Color,
5406 pub width: f32,
5407 pub item_radius: f32,
5408 pub indicator_opacity: f32,
5409 pub item_spacing: f32,
5410 pub indicator_width: f32,
5411 pub indicator_height: f32,
5412}
5413
5414impl Default for NavigationRailConfig {
5415 fn default() -> Self {
5416 Self {
5417 modifier: Modifier::new(),
5418 container_color: NavigationRailDefaults::container_color(),
5419 selected_icon_color: NavigationRailDefaults::selected_icon_color(),
5420 selected_text_color: NavigationRailDefaults::selected_text_color(),
5421 unselected_icon_color: NavigationRailDefaults::unselected_icon_color(),
5422 unselected_text_color: NavigationRailDefaults::unselected_text_color(),
5423 indicator_color: NavigationRailDefaults::indicator_color(),
5424 width: NavigationRailDefaults::WIDTH,
5425 item_radius: NavigationRailDefaults::ITEM_RADIUS,
5426 indicator_opacity: NavigationRailDefaults::ITEM_ACTIVE_INDICATOR_OPACITY,
5427 item_spacing: NavigationRailDefaults::ITEM_SPACING,
5428 indicator_width: NavigationRailDefaults::ACTIVE_INDICATOR_WIDTH,
5429 indicator_height: NavigationRailDefaults::ACTIVE_INDICATOR_HEIGHT,
5430 }
5431 }
5432}
5433
5434#[derive(Clone, Debug)]
5436pub struct ScaffoldConfig {
5437 pub modifier: Modifier,
5438 pub container_color: Color,
5439 pub top_bar_height: f32,
5440 pub bottom_bar_height: f32,
5441 pub fab_margin: f32,
5442}
5443
5444impl Default for ScaffoldConfig {
5445 fn default() -> Self {
5446 Self {
5447 modifier: Modifier::new(),
5448 container_color: ScaffoldDefaults::container_color(),
5449 top_bar_height: ScaffoldDefaults::TOP_BAR_HEIGHT,
5450 bottom_bar_height: ScaffoldDefaults::BOTTOM_BAR_HEIGHT,
5451 fab_margin: ScaffoldDefaults::FAB_MARGIN,
5452 }
5453 }
5454}
5455
5456#[derive(Clone, Debug)]
5458pub struct NavigationDrawerConfig {
5459 pub modifier: Modifier,
5460 pub container_color: Color,
5461 pub content_color: Color,
5462 pub scrim_color: Color,
5463 pub tonal_elevation: f32,
5464 pub width: f32,
5465 pub shape_radius: f32,
5466}
5467
5468impl Default for NavigationDrawerConfig {
5469 fn default() -> Self {
5470 Self {
5471 modifier: Modifier::new(),
5472 container_color: NavigationDrawerDefaults::container_color(),
5473 content_color: NavigationDrawerDefaults::content_color(),
5474 scrim_color: NavigationDrawerDefaults::scrim_color(),
5475 tonal_elevation: NavigationDrawerDefaults::TONAL_ELEVATION,
5476 width: NavigationDrawerDefaults::WIDTH,
5477 shape_radius: NavigationDrawerDefaults::SHAPE_RADIUS,
5478 }
5479 }
5480}
5481
5482#[derive(Clone, Debug)]
5484pub struct BottomSheetConfig {
5485 pub modifier: Modifier,
5486 pub container_color: Color,
5487 pub content_color: Color,
5488 pub scrim_color: Color,
5489 pub tonal_elevation: f32,
5490 pub shadow_elevation: f32,
5491 pub drag_handle_color: Color,
5492 pub shape_radius: f32,
5493 pub max_width: f32,
5494 pub drag_handle_width: f32,
5495 pub drag_handle_height: f32,
5496 pub peek_height: f32,
5497 pub gestures_enabled: bool,
5498}
5499
5500impl Default for BottomSheetConfig {
5501 fn default() -> Self {
5502 Self {
5503 modifier: Modifier::new(),
5504 container_color: BottomSheetDefaults::container_color(),
5505 content_color: BottomSheetDefaults::content_color(),
5506 scrim_color: BottomSheetDefaults::scrim_color(),
5507 tonal_elevation: BottomSheetDefaults::TONAL_ELEVATION,
5508 shadow_elevation: 0.0,
5509 drag_handle_color: BottomSheetDefaults::drag_handle_color(),
5510 shape_radius: BottomSheetDefaults::SHAPE_RADIUS,
5511 max_width: BottomSheetDefaults::MAX_WIDTH,
5512 drag_handle_width: BottomSheetDefaults::DRAG_HANDLE_WIDTH,
5513 drag_handle_height: BottomSheetDefaults::DRAG_HANDLE_HEIGHT,
5514 peek_height: BottomSheetDefaults::PEEK_HEIGHT,
5515 gestures_enabled: true,
5516 }
5517 }
5518}
5519
5520#[derive(Clone, Copy, Debug)]
5522pub struct SearchBarColors {
5523 pub container_color: Color,
5524 pub active_container_color: Color,
5525 pub divider_color: Color,
5526 pub content_color: Color,
5527 pub placeholder_color: Color,
5528 pub scrim_color: Color,
5529}
5530
5531impl SearchBarColors {
5532 pub fn container(&self, active: bool) -> Color {
5533 if active {
5534 self.active_container_color
5535 } else {
5536 self.container_color
5537 }
5538 }
5539}
5540
5541impl Default for SearchBarColors {
5542 fn default() -> Self {
5543 Self {
5544 container_color: SearchBarDefaults::container_color(),
5545 active_container_color: SearchBarDefaults::active_container_color(),
5546 divider_color: SearchBarDefaults::divider_color(),
5547 content_color: SearchBarDefaults::content_color(),
5548 placeholder_color: SearchBarDefaults::placeholder_color(),
5549 scrim_color: SearchBarDefaults::scrim_color(),
5550 }
5551 }
5552}
5553
5554#[derive(Clone, Copy, Debug)]
5556pub struct AppBarWithSearchColors {
5557 pub search_bar_colors: SearchBarColors,
5558 pub scrolled_search_bar_container_color: Color,
5559 pub app_bar_container_color: Color,
5560 pub scrolled_app_bar_container_color: Color,
5561 pub navigation_icon_content_color: Color,
5562 pub action_icon_content_color: Color,
5563}
5564
5565impl AppBarWithSearchColors {
5566 pub fn search_bar_container(&self, scroll_fraction: f32) -> Color {
5567 lerp_color(
5568 self.search_bar_colors.container_color,
5569 self.scrolled_search_bar_container_color,
5570 scroll_fraction.clamp(0.0, 1.0),
5571 )
5572 }
5573 pub fn app_bar_container(&self, scroll_fraction: f32) -> Color {
5574 lerp_color(
5575 self.app_bar_container_color,
5576 self.scrolled_app_bar_container_color,
5577 scroll_fraction.clamp(0.0, 1.0),
5578 )
5579 }
5580}
5581
5582impl Default for AppBarWithSearchColors {
5583 fn default() -> Self {
5584 Self {
5585 search_bar_colors: SearchBarColors::default(),
5586 scrolled_search_bar_container_color: SearchBarDefaults::scrolled_container_color(),
5587 app_bar_container_color: SearchBarDefaults::app_bar_container_color(),
5588 scrolled_app_bar_container_color: SearchBarDefaults::scrolled_app_bar_container_color(),
5589 navigation_icon_content_color: SearchBarDefaults::navigation_icon_content_color(),
5590 action_icon_content_color: SearchBarDefaults::action_icon_content_color(),
5591 }
5592 }
5593}
5594
5595#[derive(Clone, Debug)]
5597pub struct SearchBarConfig {
5598 pub modifier: Modifier,
5599 pub colors: SearchBarColors,
5600 pub height: f32,
5601 pub shape_radius: f32,
5602 pub active_shape_radius: f32,
5603 pub expanded_width: f32,
5604 pub collapsed_width: f32,
5605 pub tonal_elevation: f32,
5606 pub shadow_elevation: f32,
5607 pub window_insets: WindowInsets,
5608 pub content_padding: PaddingValues,
5609 pub min_width: f32,
5610 pub max_width: f32,
5611}
5612
5613impl Default for SearchBarConfig {
5614 fn default() -> Self {
5615 Self {
5616 modifier: Modifier::new(),
5617 colors: SearchBarColors::default(),
5618 height: SearchBarDefaults::HEIGHT,
5619 shape_radius: SearchBarDefaults::SHAPE_RADIUS,
5620 active_shape_radius: SearchBarDefaults::ACTIVE_SHAPE_RADIUS,
5621 expanded_width: SearchBarDefaults::EXPANDED_WIDTH,
5622 collapsed_width: SearchBarDefaults::COLLAPSED_WIDTH,
5623 tonal_elevation: SearchBarDefaults::TONAL_ELEVATION,
5624 shadow_elevation: SearchBarDefaults::SHADOW_ELEVATION,
5625 window_insets: WindowInsets::default(),
5626 content_padding: SearchBarDefaults::CONTENT_PADDING,
5627 min_width: SearchBarDefaults::MIN_WIDTH,
5628 max_width: SearchBarDefaults::MAX_WIDTH,
5629 }
5630 }
5631}
5632
5633#[derive(Clone, Debug)]
5635pub struct ExpandedFullScreenSearchBarConfig {
5636 pub modifier: Modifier,
5637 pub colors: SearchBarColors,
5638 pub collapsed_shape_radius: f32,
5639 pub tonal_elevation: f32,
5640 pub shadow_elevation: f32,
5641 pub window_insets: WindowInsets,
5642 pub scrim_color: Color,
5643}
5644
5645impl Default for ExpandedFullScreenSearchBarConfig {
5646 fn default() -> Self {
5647 Self {
5648 modifier: Modifier::new(),
5649 colors: SearchBarColors::default(),
5650 collapsed_shape_radius: SearchBarDefaults::SHAPE_RADIUS,
5651 tonal_elevation: SearchBarDefaults::TONAL_ELEVATION,
5652 shadow_elevation: SearchBarDefaults::SHADOW_ELEVATION,
5653 window_insets: WindowInsets::default(),
5654 scrim_color: SearchBarDefaults::scrim_color(),
5655 }
5656 }
5657}
5658
5659#[derive(Clone, Debug)]
5661pub struct ExpandedDockedSearchBarConfig {
5662 pub modifier: Modifier,
5663 pub colors: SearchBarColors,
5664 pub shape_radius: f32,
5665 pub dropdown_shape_radius: f32,
5666 pub dropdown_gap_size: f32,
5667 pub dropdown_scrim_color: Color,
5668 pub tonal_elevation: f32,
5669 pub shadow_elevation: f32,
5670}
5671
5672impl Default for ExpandedDockedSearchBarConfig {
5673 fn default() -> Self {
5674 Self {
5675 modifier: Modifier::new(),
5676 colors: SearchBarColors::default(),
5677 shape_radius: SearchBarDefaults::DOCKED_SHAPE_RADIUS,
5678 dropdown_shape_radius: SearchBarDefaults::DROPDOWN_SHAPE_RADIUS,
5679 dropdown_gap_size: SearchBarDefaults::DROPDOWN_GAP_SIZE,
5680 dropdown_scrim_color: SearchBarDefaults::dropdown_scrim_color(),
5681 tonal_elevation: SearchBarDefaults::TONAL_ELEVATION,
5682 shadow_elevation: SearchBarDefaults::SHADOW_ELEVATION,
5683 }
5684 }
5685}
5686
5687#[derive(Clone, Debug)]
5689pub struct AppBarWithSearchConfig {
5690 pub modifier: Modifier,
5691 pub colors: AppBarWithSearchColors,
5692 pub height: f32,
5693 pub shape_radius: f32,
5694 pub tonal_elevation: f32,
5695 pub shadow_elevation: f32,
5696 pub content_padding: PaddingValues,
5697 pub window_insets: WindowInsets,
5698 pub scroll_fraction: f32,
5699 pub scroll_offset: f32,
5700}
5701
5702impl Default for AppBarWithSearchConfig {
5703 fn default() -> Self {
5704 Self {
5705 modifier: Modifier::new(),
5706 colors: AppBarWithSearchColors::default(),
5707 height: SearchBarDefaults::HEIGHT,
5708 shape_radius: SearchBarDefaults::SHAPE_RADIUS,
5709 tonal_elevation: SearchBarDefaults::TONAL_ELEVATION,
5710 shadow_elevation: SearchBarDefaults::SHADOW_ELEVATION,
5711 content_padding: SearchBarDefaults::CONTENT_PADDING,
5712 window_insets: WindowInsets::default(),
5713 scroll_fraction: 0.0,
5714 scroll_offset: 0.0,
5715 }
5716 }
5717}
5718
5719pub struct SearchBarScrollBehavior {
5721 pub collapsed_offset: Signal<f32>,
5722 pub height: f32,
5723 pub collapsed_height: f32,
5724 _pending: Rc<Cell<f32>>,
5725}
5726
5727impl SearchBarScrollBehavior {
5728 pub fn new(height: f32, collapsed_height: f32) -> Self {
5729 Self {
5730 collapsed_offset: signal(0.0),
5731 height,
5732 collapsed_height,
5733 _pending: Rc::new(Cell::new(0.0)),
5734 }
5735 }
5736
5737 pub fn offset(&self) -> f32 {
5738 self.collapsed_offset.get()
5739 }
5740
5741 pub fn nested_scroll_connection(&self) -> NestedScrollConnection {
5742 let offset = self.collapsed_offset.clone();
5743 let max_offset = self.height - self.collapsed_height;
5744 NestedScrollConnection::new().on_pre_scroll(move |delta: Vec2, _source| {
5745 let cur = offset.get();
5746 let new = (cur - delta.y).clamp(-max_offset, 0.0);
5747 let consumed = cur - new;
5748 offset.set(new);
5749 request_frame();
5750 Vec2 {
5751 x: 0.0,
5752 y: consumed,
5753 }
5754 })
5755 }
5756}
5757
5758#[derive(Clone, Debug)]
5760pub struct DropdownMenuConfig {
5761 pub modifier: Modifier,
5762 pub container_color: Color,
5763 pub item_text_color: Color,
5764 pub disabled_item_text_color: Color,
5765 pub divider_color: Color,
5766 pub min_width: f32,
5767 pub item_height: f32,
5768 pub max_width: f32,
5769 pub shadow_elevation: Option<f32>,
5770 pub tonal_elevation: f32,
5771 pub border: Option<(f32, Color, f32)>,
5772 pub shape_radius: Option<f32>,
5773 pub offset_x: f32,
5774 pub offset_y: f32,
5775 pub vertical_margin: f32,
5776}
5777
5778impl Default for DropdownMenuConfig {
5779 fn default() -> Self {
5780 Self {
5781 modifier: Modifier::new(),
5782 container_color: DropdownMenuDefaults::container_color(),
5783 item_text_color: DropdownMenuDefaults::item_text_color(),
5784 disabled_item_text_color: DropdownMenuDefaults::disabled_item_text_color(),
5785 divider_color: DropdownMenuDefaults::divider_color(),
5786 min_width: DropdownMenuDefaults::MIN_WIDTH,
5787 item_height: DropdownMenuDefaults::ITEM_HEIGHT,
5788 max_width: DropdownMenuDefaults::MAX_WIDTH,
5789 shadow_elevation: None,
5790 tonal_elevation: 0.0,
5791 border: None,
5792 shape_radius: None,
5793 offset_x: 0.0,
5794 offset_y: 0.0,
5795 vertical_margin: DropdownMenuDefaults::VERTICAL_MARGIN,
5796 }
5797 }
5798}
5799
5800#[derive(Clone, Debug)]
5802pub struct TooltipConfig {
5803 pub modifier: Modifier,
5804 pub container_color: Color,
5805 pub content_color: Color,
5806 pub offset_y: f32,
5807 pub horizontal_padding: f32,
5808 pub vertical_padding: f32,
5809 pub has_action: bool,
5810 pub enable_user_input: bool,
5811 pub focusable: bool,
5812 pub max_width: f32,
5813 pub tonal_elevation: f32,
5814 pub shadow_elevation: f32,
5815}
5816
5817impl Default for TooltipConfig {
5818 fn default() -> Self {
5819 Self {
5820 modifier: Modifier::new(),
5821 container_color: TooltipDefaults::container_color(),
5822 content_color: TooltipDefaults::content_color(),
5823 offset_y: TooltipDefaults::OFFSET_Y,
5824 horizontal_padding: TooltipDefaults::HORIZONTAL_PADDING,
5825 vertical_padding: TooltipDefaults::VERTICAL_PADDING,
5826 has_action: false,
5827 enable_user_input: true,
5828 focusable: false,
5829 max_width: TooltipDefaults::MAX_WIDTH,
5830 tonal_elevation: 0.0,
5831 shadow_elevation: 0.0,
5832 }
5833 }
5834}
5835
5836#[derive(Clone, Debug)]
5838pub struct SwipeToDismissConfig {
5839 pub modifier: Modifier,
5840 pub dismiss_threshold: f32,
5841 pub dismissed_offset: f32,
5842 pub animation_spec: AnimationSpec,
5843 pub gestures_enabled: bool,
5844 pub enable_dismiss_from_start_to_end: bool,
5845 pub enable_dismiss_from_end_to_start: bool,
5846}
5847
5848impl Default for SwipeToDismissConfig {
5849 fn default() -> Self {
5850 Self {
5851 modifier: Modifier::new(),
5852 dismiss_threshold: SwipeToDismissDefaults::DISMISS_THRESHOLD,
5853 dismissed_offset: SwipeToDismissDefaults::DISMISSED_OFFSET,
5854 animation_spec: AnimationSpec::spring_gentle(),
5855 gestures_enabled: true,
5856 enable_dismiss_from_start_to_end: true,
5857 enable_dismiss_from_end_to_start: true,
5858 }
5859 }
5860}
5861
5862#[derive(Clone, Debug)]
5864pub struct PullToRefreshConfig {
5865 pub modifier: Modifier,
5866 pub indicator_color: Color,
5867 pub threshold: f32,
5868 pub content_alignment: AlignItems,
5869}
5870
5871impl Default for PullToRefreshConfig {
5872 fn default() -> Self {
5873 Self {
5874 modifier: Modifier::new(),
5875 indicator_color: PullToRefreshDefaults::indicator_color(),
5876 threshold: PullToRefreshDefaults::THRESHOLD,
5877 content_alignment: AlignItems::FLEX_START,
5878 }
5879 }
5880}