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 source: Rc<MutableInteractionSource> = config
1379 .interaction_source
1380 .clone()
1381 .map(Rc::new)
1382 .unwrap_or_else(|| remember(MutableInteractionSource::new));
1383
1384 let mut m = Modifier::new()
1385 .height(56.0)
1386 .min_width(80.0)
1387 .background(bg)
1388 .state_colors(StateColors {
1389 default: Color::TRANSPARENT,
1390 hovered: config.content_color.with_alpha_f32(0.08),
1391 pressed: config.content_color.with_alpha_f32(0.12),
1392 disabled: theme().on_surface.with_alpha_f32(0.12),
1393 })
1394 .state_elevation(config.state_elevation)
1395 .clip_rounded(FABDefaults::SHAPE_RADIUS)
1396 .padding_values(PaddingValues {
1397 left: 16.0,
1398 right: 20.0,
1399 top: 0.0,
1400 bottom: 0.0,
1401 })
1402 .align_items(AlignItems::CENTER);
1403
1404 m = m.interaction_source(&*source);
1405 if is_enabled {
1406 m = m.clickable().on_click(move || on_click());
1407 }
1408 m = m.then(config.modifier);
1409 Row(m).child((
1410 icon.unwrap_or(Box(Modifier::new())),
1411 Box(Modifier::new()
1412 .width(if has_icon { 12.0 } else { 0.0 })
1413 .fill_max_height()),
1414 Text(label)
1415 .color(content_color)
1416 .size(th.typography.label_large)
1417 .single_line(),
1418 ))
1419}
1420
1421#[derive(Clone, Debug)]
1423pub struct DividerConfig {
1424 pub modifier: Modifier,
1425 pub thickness: f32,
1426 pub color: Color,
1427}
1428
1429impl Default for DividerConfig {
1430 fn default() -> Self {
1431 Self {
1432 modifier: Modifier::new(),
1433 thickness: DividerDefaults::THICKNESS,
1434 color: DividerDefaults::color(),
1435 }
1436 }
1437}
1438
1439pub fn HorizontalDivider(config: DividerConfig) -> View {
1442 Box(Modifier::new()
1443 .min_width(200.0)
1444 .height(config.thickness)
1445 .background(config.color)
1446 .then(config.modifier))
1447}
1448
1449#[deprecated(since = "0.19.5", note = "renamed to HorizontalDivider")]
1450pub fn Divider(config: DividerConfig) -> View {
1451 HorizontalDivider(config)
1452}
1453
1454pub fn VerticalDivider(config: DividerConfig) -> View {
1456 Box(Modifier::new()
1457 .width(config.thickness)
1458 .fill_max_height()
1459 .background(config.color)
1460 .then(config.modifier))
1461}
1462
1463#[derive(Clone, Debug)]
1465pub struct BadgeConfig {
1466 pub modifier: Modifier,
1467 pub container_color: Color,
1468 pub content_color: Color,
1469}
1470
1471impl Default for BadgeConfig {
1472 fn default() -> Self {
1473 Self {
1474 modifier: Modifier::new(),
1475 container_color: BadgeDefaults::container_color(),
1476 content_color: BadgeDefaults::content_color(),
1477 }
1478 }
1479}
1480
1481pub fn Badge(content: Option<View>, config: BadgeConfig) -> View {
1484 match content {
1485 None => Box(Modifier::new()
1486 .size(BadgeDefaults::DOT_SIZE, BadgeDefaults::DOT_SIZE)
1487 .background(config.container_color)
1488 .clip_rounded(BadgeDefaults::DOT_SIZE * 0.5)
1489 .flex_shrink(0.0)
1490 .then(config.modifier)),
1491 Some(view) => Box(Modifier::new()
1492 .min_width(BadgeDefaults::LABEL_MIN_WIDTH)
1493 .height(BadgeDefaults::LABEL_HEIGHT)
1494 .background(config.container_color)
1495 .clip_rounded(BadgeDefaults::LABEL_HEIGHT * 0.5)
1496 .padding_values(PaddingValues {
1497 left: 4.0,
1498 right: 4.0,
1499 top: 0.0,
1500 bottom: 0.0,
1501 })
1502 .align_items(AlignItems::CENTER)
1503 .justify_content(JustifyContent::CENTER)
1504 .flex_shrink(0.0)
1505 .then(config.modifier))
1506 .child(with_content_color(config.content_color, move || view)),
1507 }
1508}
1509
1510#[derive(Clone, Debug)]
1512pub struct BadgedBoxConfig {
1513 pub modifier: Modifier,
1514 pub dot_offset_x: f32,
1516 pub dot_offset_y: f32,
1518 pub content_offset_x: f32,
1520 pub content_offset_y: f32,
1522 pub has_content: bool,
1524}
1525
1526impl Default for BadgedBoxConfig {
1527 fn default() -> Self {
1528 Self {
1529 modifier: Modifier::new(),
1530 dot_offset_x: BadgeDefaults::DOT_OFFSET_X,
1531 dot_offset_y: BadgeDefaults::DOT_OFFSET_Y,
1532 content_offset_x: BadgeDefaults::CONTENT_OFFSET_X,
1533 content_offset_y: BadgeDefaults::CONTENT_OFFSET_Y,
1534 has_content: false,
1535 }
1536 }
1537}
1538
1539pub fn BadgedBox(badge: View, content: View, config: BadgedBoxConfig) -> View {
1541 let (top, right) = if config.has_content {
1542 (
1543 config.content_offset_y - BadgeDefaults::LABEL_HEIGHT, config.content_offset_x - BadgeDefaults::LABEL_MIN_WIDTH, )
1546 } else {
1547 (
1548 config.dot_offset_y - BadgeDefaults::DOT_SIZE, config.dot_offset_x - BadgeDefaults::DOT_SIZE, )
1551 };
1552
1553 Box(config.modifier.flex_shrink(0.0)).child((
1554 content,
1555 Box(Modifier::new()
1556 .absolute()
1557 .offset(None, Some(top), Some(right), None)
1558 .flex_shrink(0.0)
1559 .hit_passthrough())
1560 .child(badge),
1561 ))
1562}
1563
1564#[derive(Clone, Debug)]
1567pub struct ListItemColors {
1568 pub container_color: Color,
1569 pub headline_color: Color,
1570 pub supporting_color: Color,
1571 pub overline_color: Color,
1572 pub leading_icon_color: Color,
1573 pub trailing_icon_color: Color,
1574
1575 pub disabled_container_color: Color,
1576 pub disabled_headline_color: Color,
1577 pub disabled_supporting_color: Color,
1578 pub disabled_overline_color: Color,
1579 pub disabled_leading_icon_color: Color,
1580 pub disabled_trailing_icon_color: Color,
1581
1582 pub selected_container_color: Color,
1583 pub selected_headline_color: Color,
1584 pub selected_supporting_color: Color,
1585 pub selected_overline_color: Color,
1586 pub selected_leading_icon_color: Color,
1587 pub selected_trailing_icon_color: Color,
1588
1589 pub dragged_container_color: Color,
1590 pub dragged_headline_color: Color,
1591 pub dragged_supporting_color: Color,
1592 pub dragged_overline_color: Color,
1593 pub dragged_leading_icon_color: Color,
1594 pub dragged_trailing_icon_color: Color,
1595}
1596
1597impl ListItemColors {
1598 pub fn container(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1599 if !enabled {
1600 self.disabled_container_color
1601 } else if dragged {
1602 self.dragged_container_color
1603 } else if selected {
1604 self.selected_container_color
1605 } else {
1606 self.container_color
1607 }
1608 }
1609 pub fn headline(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1610 if !enabled {
1611 self.disabled_headline_color
1612 } else if dragged {
1613 self.dragged_headline_color
1614 } else if selected {
1615 self.selected_headline_color
1616 } else {
1617 self.headline_color
1618 }
1619 }
1620 pub fn supporting(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1621 if !enabled {
1622 self.disabled_supporting_color
1623 } else if dragged {
1624 self.dragged_supporting_color
1625 } else if selected {
1626 self.selected_supporting_color
1627 } else {
1628 self.supporting_color
1629 }
1630 }
1631 pub fn overline(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1632 if !enabled {
1633 self.disabled_overline_color
1634 } else if dragged {
1635 self.dragged_overline_color
1636 } else if selected {
1637 self.selected_overline_color
1638 } else {
1639 self.overline_color
1640 }
1641 }
1642 pub fn leading_icon(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1643 if !enabled {
1644 self.disabled_leading_icon_color
1645 } else if dragged {
1646 self.dragged_leading_icon_color
1647 } else if selected {
1648 self.selected_leading_icon_color
1649 } else {
1650 self.leading_icon_color
1651 }
1652 }
1653 pub fn trailing_icon(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1654 if !enabled {
1655 self.disabled_trailing_icon_color
1656 } else if dragged {
1657 self.dragged_trailing_icon_color
1658 } else if selected {
1659 self.selected_trailing_icon_color
1660 } else {
1661 self.trailing_icon_color
1662 }
1663 }
1664}
1665
1666impl Default for ListItemColors {
1667 fn default() -> Self {
1668 Self {
1669 container_color: Color::TRANSPARENT,
1670 headline_color: ListItemDefaults::headline_color(),
1671 supporting_color: ListItemDefaults::supporting_color(),
1672 overline_color: ListItemDefaults::overline_color(),
1673 leading_icon_color: ListItemDefaults::leading_icon_color(),
1674 trailing_icon_color: ListItemDefaults::trailing_icon_color(),
1675 disabled_container_color: ListItemDefaults::disabled_container_color(),
1676 disabled_headline_color: ListItemDefaults::disabled_headline_color(),
1677 disabled_supporting_color: ListItemDefaults::disabled_supporting_color(),
1678 disabled_overline_color: ListItemDefaults::disabled_overline_color(),
1679 disabled_leading_icon_color: ListItemDefaults::disabled_leading_icon_color(),
1680 disabled_trailing_icon_color: ListItemDefaults::disabled_trailing_icon_color(),
1681 selected_container_color: ListItemDefaults::selected_container_color(),
1682 selected_headline_color: ListItemDefaults::selected_headline_color(),
1683 selected_supporting_color: ListItemDefaults::selected_supporting_color(),
1684 selected_overline_color: ListItemDefaults::selected_overline_color(),
1685 selected_leading_icon_color: ListItemDefaults::selected_leading_icon_color(),
1686 selected_trailing_icon_color: ListItemDefaults::selected_trailing_icon_color(),
1687 dragged_container_color: ListItemDefaults::dragged_container_color(),
1688 dragged_headline_color: ListItemDefaults::dragged_headline_color(),
1689 dragged_supporting_color: ListItemDefaults::dragged_supporting_color(),
1690 dragged_overline_color: ListItemDefaults::dragged_overline_color(),
1691 dragged_leading_icon_color: ListItemDefaults::dragged_leading_icon_color(),
1692 dragged_trailing_icon_color: ListItemDefaults::dragged_trailing_icon_color(),
1693 }
1694 }
1695}
1696
1697#[derive(Clone, Debug)]
1699pub struct ListItemConfig {
1700 pub modifier: Modifier,
1701 pub enabled: bool,
1703 pub selected: bool,
1704 pub colors: ListItemColors,
1705 pub state_colors: StateColors,
1706 pub tonal_elevation: f32,
1707 pub shadow_elevation: f32,
1708 pub shape_radius: f32,
1709 pub shape_radii: Option<[f32; 4]>,
1711 pub horizontal_padding: f32,
1712 pub trailing_padding: f32,
1713 pub one_line_height: f32,
1714 pub two_line_height: f32,
1715 pub three_line_height: f32,
1716 pub interaction_source: Option<MutableInteractionSource>,
1717}
1718
1719impl Default for ListItemConfig {
1720 fn default() -> Self {
1721 Self {
1722 modifier: Modifier::new(),
1723 enabled: true,
1724 selected: false,
1725 colors: ListItemColors::default(),
1726 state_colors: ListItemDefaults::state_colors_default(),
1727 tonal_elevation: 0.0,
1728 shadow_elevation: 0.0,
1729 shape_radius: 0.0,
1730 shape_radii: None,
1731 horizontal_padding: ListItemDefaults::HORIZONTAL_PADDING,
1732 trailing_padding: ListItemDefaults::TRAILING_PADDING,
1733 one_line_height: ListItemDefaults::ONE_LINE_HEIGHT,
1734 two_line_height: ListItemDefaults::TWO_LINE_HEIGHT,
1735 three_line_height: ListItemDefaults::THREE_LINE_HEIGHT,
1736 interaction_source: None,
1737 }
1738 }
1739}
1740
1741static LISTITEM_COUNTER: AtomicU64 = AtomicU64::new(0);
1742
1743pub fn ListItem(
1746 headline: impl Into<String>,
1747 supporting_text: Option<String>,
1748 overline_text: Option<String>,
1749 leading: Option<View>,
1750 trailing: Option<View>,
1751 on_click: Option<Rc<dyn Fn()>>,
1752 on_long_click: Option<Rc<dyn Fn()>>,
1753 config: ListItemConfig,
1754) -> View {
1755 let th = theme();
1756 let is_enabled = config.enabled;
1757 let is_selected = config.selected;
1758 let c = &config.colors;
1759 let id = remember(|| LISTITEM_COUNTER.fetch_add(1, Ordering::Relaxed));
1760 let spec = th.motion.color;
1761
1762 let hd_col = animate_color(
1763 format!("li_hd_{}", id),
1764 c.headline(is_enabled, is_selected, false),
1765 spec,
1766 );
1767 let sp_col = animate_color(
1768 format!("li_sp_{}", id),
1769 c.supporting(is_enabled, is_selected, false),
1770 spec,
1771 );
1772 let ol_col = animate_color(
1773 format!("li_ol_{}", id),
1774 c.overline(is_enabled, is_selected, false),
1775 spec,
1776 );
1777 let ld_col = animate_color(
1778 format!("li_ld_{}", id),
1779 c.leading_icon(is_enabled, is_selected, false),
1780 spec,
1781 );
1782 let tr_col = animate_color(
1783 format!("li_tr_{}", id),
1784 c.trailing_icon(is_enabled, is_selected, false),
1785 spec,
1786 );
1787 let bg = animate_color(
1788 format!("li_bg_{}", id),
1789 c.container(is_enabled, is_selected, false),
1790 spec,
1791 );
1792
1793 let line_count = match (overline_text.is_some(), supporting_text.is_some()) {
1794 (true, true) => 3,
1795 (true, false) | (false, true) => 2,
1796 (false, false) => 1,
1797 };
1798 let min_h = match line_count {
1799 3 => config.three_line_height,
1800 2 => config.two_line_height,
1801 _ => config.one_line_height,
1802 };
1803 let top_bottom_padding = match line_count {
1804 3 => 12.0,
1805 _ => 8.0,
1806 };
1807
1808 let vert_align = if min_h >= config.three_line_height {
1809 AlignItems::START
1810 } else {
1811 AlignItems::CENTER
1812 };
1813
1814 let li_source: Rc<MutableInteractionSource> = config
1815 .interaction_source
1816 .clone()
1817 .map(Rc::new)
1818 .unwrap_or_else(|| remember(MutableInteractionSource::new));
1819
1820 let mut modifier = Modifier::new()
1821 .min_width(200.0)
1822 .min_height(min_h)
1823 .background(bg);
1824 match config.shape_radii {
1825 Some(r) => modifier = modifier.clip_rounded_radii(r),
1826 None => modifier = modifier.clip_rounded(config.shape_radius),
1827 }
1828 modifier = modifier
1829 .state_colors(config.state_colors)
1830 .padding_values(PaddingValues {
1831 left: config.horizontal_padding,
1832 right: config.trailing_padding,
1833 top: top_bottom_padding,
1834 bottom: top_bottom_padding,
1835 })
1836 .align_items(vert_align)
1837 .interaction_source(&*li_source)
1838 .then(config.modifier);
1839
1840 if config.tonal_elevation > 0.0 {
1841 modifier = modifier.state_elevation(StateElevation {
1842 default: config.tonal_elevation,
1843 hovered: config.tonal_elevation,
1844 pressed: config.tonal_elevation,
1845 disabled: 0.0,
1846 });
1847 }
1848 if config.shadow_elevation > 0.0 {
1849 modifier = modifier.shadow(config.shadow_elevation, 0.0);
1850 }
1851
1852 if on_click.is_some() || on_long_click.is_some() {
1853 modifier = modifier.clickable();
1854 if let Some(cb) = on_click {
1855 let cb = cb.clone();
1856 modifier = modifier.on_click(move || {
1857 if is_enabled {
1858 cb();
1859 }
1860 });
1861 }
1862 if let Some(cb) = &on_long_click {
1863 let cb = cb.clone();
1864 modifier = modifier.on_long_click(move || {
1865 if is_enabled {
1866 cb();
1867 }
1868 });
1869 }
1870 }
1871
1872 let wrap_icon = |color: Color, v: View| -> View { with_content_color(color, move || v) };
1873
1874 Row(modifier).child((
1875 leading
1876 .map(|v| {
1877 Box(Modifier::new().padding_values(PaddingValues {
1878 left: 0.0,
1879 right: 16.0,
1880 top: 0.0,
1881 bottom: 0.0,
1882 }))
1883 .child(wrap_icon(ld_col, v))
1884 })
1885 .unwrap_or(Box(Modifier::new())),
1886 Column(
1887 Modifier::new()
1888 .flex_grow(1.0)
1889 .justify_content(JustifyContent::CENTER),
1890 )
1891 .child((
1892 overline_text
1893 .map(|ot| {
1894 Text(ot)
1895 .color(ol_col)
1896 .size(th.typography.label_small)
1897 .single_line()
1898 })
1899 .unwrap_or(Box(Modifier::new())),
1900 Text(headline)
1901 .color(hd_col)
1902 .size(th.typography.body_large)
1903 .single_line(),
1904 supporting_text
1905 .map(|st| {
1906 Text(st)
1907 .color(sp_col)
1908 .size(th.typography.body_medium)
1909 .max_lines(2)
1910 .overflow_ellipsize()
1911 })
1912 .unwrap_or(Box(Modifier::new())),
1913 )),
1914 trailing
1915 .map(|v| {
1916 Box(Modifier::new().padding_values(PaddingValues {
1917 left: 16.0,
1918 right: 0.0,
1919 top: 0.0,
1920 bottom: 0.0,
1921 }))
1922 .child(wrap_icon(tr_col, v))
1923 })
1924 .unwrap_or(Box(Modifier::new())),
1925 ))
1926}
1927
1928pub fn SelectableListItem(
1931 headline: impl Into<String>,
1932 selected: bool,
1933 supporting_text: Option<String>,
1934 overline_text: Option<String>,
1935 leading: Option<View>,
1936 trailing: Option<View>,
1937 on_click: Option<Rc<dyn Fn()>>,
1938 mut config: ListItemConfig,
1939) -> View {
1940 config.selected = selected;
1941 let mut m = Modifier::new().semantics(Semantics::new(Role::RadioButton));
1942 m = m.then(config.modifier);
1943 config.modifier = m;
1944 ListItem(
1945 headline,
1946 supporting_text,
1947 overline_text,
1948 leading,
1949 trailing,
1950 on_click,
1951 None,
1952 config,
1953 )
1954}
1955
1956pub fn ToggleableListItem(
1959 headline: impl Into<String>,
1960 checked: bool,
1961 on_checked_change: impl Fn(bool) + 'static,
1962 supporting_text: Option<String>,
1963 overline_text: Option<String>,
1964 leading: Option<View>,
1965 trailing: Option<View>,
1966 config: ListItemConfig,
1967) -> View {
1968 let mut cfg = config.clone();
1969 cfg.selected = checked;
1970 let cb = Rc::new(on_checked_change);
1971 let cb2 = cb.clone();
1972 let mut m = Modifier::new().semantics(Semantics::new(Role::Checkbox));
1973 m = m.then(cfg.modifier);
1974 cfg.modifier = m;
1975 ListItem(
1976 headline,
1977 supporting_text,
1978 overline_text,
1979 leading,
1980 trailing,
1981 Some(Rc::new(move || (cb2)(!checked))),
1982 None,
1983 cfg,
1984 )
1985}
1986
1987fn segmented_item_radii(index: usize, count: usize, r: f32) -> [f32; 4] {
1989 if count <= 1 {
1990 [r, r, r, r]
1991 } else if index == 0 {
1992 [0.0, 0.0, r, r]
1993 } else if index == count - 1 {
1994 [r, r, 0.0, 0.0]
1995 } else {
1996 [0.0, 0.0, 0.0, 0.0]
1997 }
1998}
1999
2000pub fn SegmentedListItem(
2002 index: usize,
2003 count: usize,
2004 headline: impl Into<String>,
2005 supporting_text: Option<String>,
2006 overline_text: Option<String>,
2007 leading: Option<View>,
2008 trailing: Option<View>,
2009 on_click: Option<Rc<dyn Fn()>>,
2010 mut config: ListItemConfig,
2011) -> View {
2012 config.shape_radii = Some(segmented_item_radii(index, count, config.shape_radius));
2013 ListItem(
2014 headline,
2015 supporting_text,
2016 overline_text,
2017 leading,
2018 trailing,
2019 on_click,
2020 None,
2021 config,
2022 )
2023}
2024
2025pub fn SegmentedSelectableListItem(
2027 index: usize,
2028 count: usize,
2029 headline: impl Into<String>,
2030 selected: bool,
2031 supporting_text: Option<String>,
2032 overline_text: Option<String>,
2033 leading: Option<View>,
2034 trailing: Option<View>,
2035 on_click: Option<Rc<dyn Fn()>>,
2036 mut config: ListItemConfig,
2037) -> View {
2038 config.selected = selected;
2039 config.shape_radii = Some(segmented_item_radii(index, count, config.shape_radius));
2040 let mut m = Modifier::new().semantics(Semantics::new(Role::RadioButton));
2041 m = m.then(config.modifier);
2042 config.modifier = m;
2043 ListItem(
2044 headline,
2045 supporting_text,
2046 overline_text,
2047 leading,
2048 trailing,
2049 on_click,
2050 None,
2051 config,
2052 )
2053}
2054
2055pub fn SegmentedToggleableListItem(
2057 index: usize,
2058 count: usize,
2059 headline: impl Into<String>,
2060 checked: bool,
2061 on_checked_change: impl Fn(bool) + 'static,
2062 supporting_text: Option<String>,
2063 overline_text: Option<String>,
2064 leading: Option<View>,
2065 trailing: Option<View>,
2066 config: ListItemConfig,
2067) -> View {
2068 let mut cfg = config.clone();
2069 cfg.selected = checked;
2070 cfg.shape_radii = Some(segmented_item_radii(index, count, cfg.shape_radius));
2071 let cb2 = Rc::new(on_checked_change);
2072 let mut m = Modifier::new().semantics(Semantics::new(Role::Checkbox));
2073 m = m.then(cfg.modifier);
2074 cfg.modifier = m;
2075 ListItem(
2076 headline,
2077 supporting_text,
2078 overline_text,
2079 leading,
2080 trailing,
2081 Some(Rc::new(move || (cb2)(!checked))),
2082 None,
2083 cfg,
2084 )
2085}
2086
2087pub struct Tab {
2089 pub label: String,
2090 pub icon: Option<View>,
2091 pub on_click: Rc<dyn Fn()>,
2092 pub enabled: bool,
2093 pub interaction_source: Option<MutableInteractionSource>,
2094}
2095
2096#[derive(Clone, Debug)]
2098pub struct TabRowConfig {
2099 pub modifier: Modifier,
2100 pub container_color: Color,
2101 pub selected_content_color: Color,
2102 pub unselected_content_color: Color,
2103 pub indicator_color: Color,
2104 pub height: f32,
2105 pub indicator_height: f32,
2106}
2107
2108impl Default for TabRowConfig {
2109 fn default() -> Self {
2110 Self {
2111 modifier: Modifier::new(),
2112 container_color: TabDefaults::container_color(),
2113 selected_content_color: TabDefaults::selected_content_color(),
2114 unselected_content_color: TabDefaults::unselected_content_color(),
2115 indicator_color: TabDefaults::indicator_color(),
2116 height: TabDefaults::HEIGHT,
2117 indicator_height: TabDefaults::INDICATOR_HEIGHT,
2118 }
2119 }
2120}
2121
2122static TABROW_COUNTER: AtomicU64 = AtomicU64::new(0);
2123
2124pub fn TabRow(selected_index: usize, tabs: Vec<Tab>, config: TabRowConfig) -> View {
2128 let th = theme();
2129 let id = remember(|| TABROW_COUNTER.fetch_add(1, Ordering::Relaxed));
2130 let default_effects = AnimationSpec::spring_crit(40.0);
2131 Column(Modifier::new().fill_max_width().then(config.modifier)).child((
2132 Row(Modifier::new()
2133 .fill_max_width()
2134 .height(config.height)
2135 .background(config.container_color)
2136 .semantics(Semantics::new(Role::Container).with_selectable_group()))
2137 .child(
2138 tabs.into_iter()
2139 .enumerate()
2140 .map(|(i, tab)| {
2141 let selected = i == selected_index;
2142 let is_enabled = tab.enabled;
2143 let color = animate_color(
2144 format!("tab_clr_{}_{}", id, i),
2145 if selected {
2146 config.selected_content_color
2147 } else {
2148 config.unselected_content_color
2149 },
2150 default_effects,
2151 );
2152 let indicator_h = animate_f32(
2153 format!("tab_ind_h_{}_{}", id, i),
2154 if selected {
2155 config.indicator_height
2156 } else {
2157 0.0
2158 },
2159 default_effects,
2160 );
2161 let cb = tab.on_click.clone();
2162 let tab_source: Rc<MutableInteractionSource> = tab
2163 .interaction_source
2164 .clone()
2165 .map(Rc::new)
2166 .unwrap_or_else(|| remember(MutableInteractionSource::new));
2167
2168 let mut tab_m = Modifier::new()
2169 .flex_grow(1.0)
2170 .fill_max_height()
2171 .interaction_source(&*tab_source)
2172 .align_items(AlignItems::CENTER)
2173 .justify_content(JustifyContent::CENTER)
2174 .state_colors(StateColors {
2175 default: Color::TRANSPARENT,
2176 hovered: th.on_surface.with_alpha_f32(0.08),
2177 pressed: th.on_surface.with_alpha_f32(0.12),
2178 disabled: Color::TRANSPARENT,
2179 })
2180 .semantics(Semantics::new(Role::Tab).with_label(&tab.label));
2181
2182 if is_enabled {
2183 tab_m = tab_m.clickable().on_click(move || cb());
2184 }
2185
2186 Column(tab_m).child((
2187 tab.icon.unwrap_or(Box(Modifier::new())),
2188 Text(tab.label)
2189 .color(color)
2190 .size(th.typography.title_small)
2191 .single_line(),
2192 Box(Modifier::new()
2193 .fill_max_width()
2194 .height(indicator_h)
2195 .background(config.indicator_color)
2196 .clip_rounded(TabDefaults::INDICATOR_CORNER)),
2197 ))
2198 })
2199 .collect::<Vec<_>>(),
2200 ),
2201 Box(Modifier::new()
2203 .fill_max_width()
2204 .height(1.0)
2205 .background(th.outline_variant)),
2206 ))
2207}
2208
2209#[derive(Clone)]
2211pub struct SegmentConfig {
2212 pub label: String,
2213 pub icon: Option<View>,
2214 pub on_click: Rc<dyn Fn()>,
2215 pub enabled: bool,
2216 pub interaction_source: Option<MutableInteractionSource>,
2217}
2218
2219impl Default for SegmentConfig {
2220 fn default() -> Self {
2221 Self {
2222 label: String::new(),
2223 icon: None,
2224 on_click: Rc::new(|| {}),
2225 enabled: true,
2226 interaction_source: None,
2227 }
2228 }
2229}
2230
2231#[derive(Clone, Debug)]
2233pub struct SegmentedButtonConfig {
2234 pub modifier: Modifier,
2235 pub border_color: Color,
2236 pub selected_container_color: Color,
2237 pub selected_content_color: Color,
2238 pub unselected_content_color: Color,
2239 pub state_colors: StateColors,
2240 pub height: f32,
2241 pub shape_radius: f32,
2242 pub content_padding: PaddingValues,
2243}
2244
2245impl Default for SegmentedButtonConfig {
2246 fn default() -> Self {
2247 Self {
2248 modifier: Modifier::new(),
2249 border_color: SegmentedButtonDefaults::border_color(),
2250 selected_container_color: SegmentedButtonDefaults::selected_container_color(),
2251 selected_content_color: SegmentedButtonDefaults::selected_content_color(),
2252 unselected_content_color: SegmentedButtonDefaults::unselected_content_color(),
2253 state_colors: SegmentedButtonDefaults::state_colors_default(),
2254 height: SegmentedButtonDefaults::HEIGHT,
2255 shape_radius: SegmentedButtonDefaults::SHAPE_RADIUS,
2256 content_padding: SegmentedButtonDefaults::CONTENT_PADDING,
2257 }
2258 }
2259}
2260
2261static SEGBUTTON_COUNTER: AtomicU64 = AtomicU64::new(0);
2262
2263pub fn SegmentedButton(
2268 selected: &[usize],
2269 segments: Vec<SegmentConfig>,
2270 config: SegmentedButtonConfig,
2271) -> View {
2272 let th = theme();
2273 let count = segments.len();
2274 let id = remember(|| SEGBUTTON_COUNTER.fetch_add(1, Ordering::Relaxed));
2275 let spec = th.motion.color;
2276 let shape_r = config.shape_radius;
2277
2278 let segment_radii = |i: usize| -> [f32; 4] {
2280 if count == 1 {
2281 [shape_r, shape_r, shape_r, shape_r]
2282 } else if i == 0 {
2283 [shape_r, 0.0, 0.0, shape_r]
2284 } else if i == count - 1 {
2285 [0.0, shape_r, shape_r, 0.0]
2286 } else {
2287 [0.0, 0.0, 0.0, 0.0]
2288 }
2289 };
2290
2291 Row(Modifier::new()
2293 .height(config.height)
2294 .border(1.0, config.border_color, shape_r)
2295 .then(config.modifier))
2296 .child(
2297 segments
2298 .into_iter()
2299 .enumerate()
2300 .map(|(i, seg)| {
2301 let is_selected = selected.contains(&i);
2302
2303 let bg = animate_color(
2304 format!("sb_bg_{}_{}", id, i),
2305 if is_selected {
2306 config.selected_container_color
2307 } else {
2308 Color::TRANSPARENT
2309 },
2310 spec,
2311 );
2312 let fg = animate_color(
2313 format!("sb_fg_{}_{}", id, i),
2314 if is_selected {
2315 config.selected_content_color
2316 } else {
2317 config.unselected_content_color
2318 },
2319 spec,
2320 );
2321
2322 let cb = seg.on_click.clone();
2323 let radii = segment_radii(i);
2324 let is_enabled = seg.enabled;
2325 let seg_source: Rc<MutableInteractionSource> = seg
2326 .interaction_source
2327 .clone()
2328 .map(Rc::new)
2329 .unwrap_or_else(|| remember(MutableInteractionSource::new));
2330
2331 let state_colors = config.state_colors;
2332 let content_modifier = Modifier::new()
2333 .flex_grow(1.0)
2334 .fill_max_height()
2335 .clip_rounded_radii(radii)
2336 .background(bg)
2337 .state_colors(state_colors)
2338 .interaction_source(&*seg_source)
2339 .align_items(AlignItems::CENTER)
2340 .justify_content(JustifyContent::CENTER)
2341 .padding_values(config.content_padding);
2342
2343 let content_modifier = if is_enabled {
2344 content_modifier.clickable().on_click(move || cb())
2345 } else {
2346 content_modifier
2347 };
2348
2349 Row(Modifier::new().flex_grow(1.0).fill_max_height()).child((
2350 Row(content_modifier).child((
2351 seg.icon.unwrap_or(Box(Modifier::new())),
2352 Text(seg.label)
2353 .color(fg)
2354 .size(th.typography.label_large)
2355 .single_line(),
2356 )),
2357 if i < count - 1 {
2358 Box(Modifier::new()
2359 .width(1.0)
2360 .fill_max_height()
2361 .background(th.outline))
2362 } else {
2363 Box(Modifier::new())
2364 },
2365 ))
2366 })
2367 .collect::<Vec<_>>(),
2368 )
2369}
2370
2371#[derive(Clone, Debug)]
2373pub struct CircularProgressIndicatorConfig {
2374 pub modifier: Modifier,
2375 pub color: Color,
2376 pub track_color: Color,
2377 pub stroke_width: f32,
2378 pub stroke_cap: StrokeCap,
2379 pub gap_size: f32,
2380}
2381
2382impl Default for CircularProgressIndicatorConfig {
2383 fn default() -> Self {
2384 Self {
2385 modifier: Modifier::new(),
2386 color: ProgressIndicatorDefaults::circular_color(),
2387 track_color: ProgressIndicatorDefaults::circular_track_color(),
2388 stroke_width: ProgressIndicatorDefaults::CIRCULAR_STROKE_WIDTH,
2389 stroke_cap: StrokeCap::Round,
2390 gap_size: 0.0,
2391 }
2392 }
2393}
2394
2395pub fn CircularProgressIndicator(
2400 value: Option<f32>,
2401 config: CircularProgressIndicatorConfig,
2402) -> View {
2403 let sz = dp_to_px(ProgressIndicatorDefaults::CIRCULAR_INDICATOR_SIZE);
2404 let stroke_px = dp_to_px(config.stroke_width);
2405 let val = value.map(|v| v.clamp(0.0, 1.0));
2406
2407 let (global_rotation, additional_rotation, sweep_val) = if value.is_none() {
2412 let shared = remember_state_with_key("circ_ind_shared", || {
2413 let mut a = AnimatedValue::new(
2414 0.0f32,
2415 AnimationSpec::tween(Duration::from_millis(6000), Easing::Linear)
2416 .repeated(RepeatableSpec::infinite()),
2417 );
2418 a.set_target(1.0);
2419 a
2420 });
2421 let mut s = shared.borrow_mut();
2422 s.update();
2423 let t = *s.get();
2424 drop(s);
2425
2426 let gv = t * 1080.0;
2427
2428 let emph = Easing::Custom(CubicBezier::new(0.05, 0.7, 0.1, 1.0));
2429 let add_kf = remember_state_with_key("circ_ind_add_kf", || KeyframesSpec {
2430 keyframes: vec![
2431 (0.0, 0.0, None),
2432 (0.05, 90.0, Some(emph)),
2433 (0.25, 90.0, None),
2434 (0.30, 180.0, None),
2435 (0.50, 180.0, None),
2436 (0.55, 270.0, None),
2437 (0.75, 270.0, None),
2438 (0.80, 360.0, None),
2439 (1.0, 360.0, None),
2440 ],
2441 });
2442 let av = add_kf.borrow().evaluate(t);
2443
2444 let std_dec = Easing::Custom(CubicBezier::new(0.2, 0.0, 0.0, 1.0));
2445 let sweep_kf = remember_state_with_key("circ_ind_sweep_kf", || KeyframesSpec {
2446 keyframes: vec![
2447 (0.0, 0.1, None),
2448 (0.5, 0.87, Some(std_dec)),
2449 (1.0, 0.1, None),
2450 ],
2451 });
2452 let sv = sweep_kf.borrow().evaluate(t);
2453
2454 (gv, av, sv)
2455 } else {
2456 (0.0, 0.0, 0.0)
2457 };
2458
2459 let indicator_size_dp = ProgressIndicatorDefaults::CIRCULAR_INDICATOR_SIZE;
2461 let adjusted_gap_dp = if config.stroke_cap == StrokeCap::Butt {
2462 config.gap_size
2463 } else {
2464 config.gap_size + config.stroke_width
2465 };
2466 let circle_dia_dp = indicator_size_dp - config.stroke_width;
2467 let gap_sweep_rad = 2.0 * adjusted_gap_dp / circle_dia_dp;
2468
2469 Box(Modifier::new().size(sz, sz).then(config.modifier).painter(
2470 move |scene: &mut Scene, rect: Rect, alpha: f32| {
2471 let mul_c = |c: Color| {
2472 Color(
2473 c.0,
2474 c.1,
2475 c.2,
2476 ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
2477 )
2478 };
2479 let cx = rect.x + rect.w * 0.5;
2480 let cy = rect.y + rect.h * 0.5;
2481 let r = (rect.w.min(rect.h)) * 0.5 - stroke_px * 0.5;
2482 let circle = Rect {
2483 x: cx - r,
2484 y: cy - r,
2485 w: r * 2.0,
2486 h: r * 2.0,
2487 };
2488
2489 match val {
2490 Some(p) => {
2491 let sweep_rad = p * std::f32::consts::TAU;
2492 let start_angle = -std::f32::consts::FRAC_PI_2;
2493 let effective_gap = gap_sweep_rad.min(sweep_rad);
2494
2495 if p > 0.0 {
2497 scene.nodes.push(SceneNode::Arc {
2498 rect: circle,
2499 start_angle,
2500 sweep_angle: sweep_rad,
2501 stroke_width: stroke_px,
2502 color: mul_c(config.color),
2503 cap: config.stroke_cap,
2504 });
2505 }
2506
2507 let track_start = start_angle + sweep_rad + effective_gap;
2509 let track_sweep = std::f32::consts::TAU - sweep_rad - 2.0 * effective_gap;
2510 if track_sweep > 0.0 {
2511 scene.nodes.push(SceneNode::Arc {
2512 rect: circle,
2513 start_angle: track_start,
2514 sweep_angle: track_sweep,
2515 stroke_width: stroke_px,
2516 color: mul_c(config.track_color),
2517 cap: config.stroke_cap,
2518 });
2519 }
2520 }
2521 None => {
2522 let radians =
2523 (global_rotation + additional_rotation) * std::f32::consts::PI / 180.0;
2524 let start_angle = -std::f32::consts::FRAC_PI_2 + radians;
2525 let sweep_rad = sweep_val * std::f32::consts::TAU;
2526 let effective_gap = gap_sweep_rad.min(sweep_rad);
2527
2528 scene.nodes.push(SceneNode::Arc {
2530 rect: circle,
2531 start_angle,
2532 sweep_angle: sweep_rad,
2533 stroke_width: stroke_px,
2534 color: mul_c(config.color),
2535 cap: config.stroke_cap,
2536 });
2537
2538 let track_start = start_angle + sweep_rad + effective_gap;
2540 let track_sweep = std::f32::consts::TAU - sweep_rad - 2.0 * effective_gap;
2541 if track_sweep > 0.0 {
2542 scene.nodes.push(SceneNode::Arc {
2543 rect: circle,
2544 start_angle: track_start,
2545 sweep_angle: track_sweep,
2546 stroke_width: stroke_px,
2547 color: mul_c(config.track_color),
2548 cap: config.stroke_cap,
2549 });
2550 }
2551 }
2552 }
2553 },
2554 ))
2555 .semantics(Semantics {
2556 role: Role::ProgressBar,
2557 label: None,
2558 focused: false,
2559 enabled: true,
2560 selectable_group: false,
2561 })
2562}
2563
2564#[derive(Clone, Debug)]
2566pub struct LinearProgressIndicatorConfig {
2567 pub modifier: Modifier,
2568 pub color: Color,
2569 pub track_color: Color,
2570 pub stroke_cap: StrokeCap,
2572 pub gap_size: f32,
2574 pub stop_size: f32,
2576}
2577
2578impl Default for LinearProgressIndicatorConfig {
2579 fn default() -> Self {
2580 Self {
2581 modifier: Modifier::new(),
2582 color: ProgressIndicatorDefaults::linear_color(),
2583 track_color: ProgressIndicatorDefaults::linear_track_color(),
2584 stroke_cap: StrokeCap::Round,
2585 gap_size: ProgressIndicatorDefaults::LINEAR_INDICATOR_GAP_SIZE,
2586 stop_size: ProgressIndicatorDefaults::LINEAR_TRACK_STOP_SIZE,
2587 }
2588 }
2589}
2590
2591pub fn LinearProgressIndicator(value: Option<f32>, config: LinearProgressIndicatorConfig) -> View {
2596 Box(Modifier::new()
2597 .fill_max_width()
2598 .height(ProgressIndicatorDefaults::LINEAR_INDICATOR_HEIGHT)
2599 .then(config.modifier)
2600 .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
2601 let mul_c = |c: Color| {
2602 Color(
2603 c.0,
2604 c.1,
2605 c.2,
2606 ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
2607 )
2608 };
2609 let track_h = rect.h;
2610 let corner = track_h * 0.5;
2611 let dot_r = dp_to_px(config.stop_size) * 0.5;
2612 let cy = rect.y + rect.h * 0.5;
2613 let t = value.unwrap_or(0.0).clamp(0.0, 1.0);
2614
2615 let cap_radius = if config.stroke_cap == StrokeCap::Butt {
2616 0.0
2617 } else {
2618 corner
2619 };
2620
2621 let gap = dp_to_px(config.gap_size)
2622 - if config.stroke_cap == StrokeCap::Butt {
2623 0.0
2624 } else {
2625 cap_radius
2626 };
2627
2628 let cap_ofs = cap_radius;
2629 let ind_end = (t * rect.w).clamp(cap_ofs, rect.w - cap_ofs);
2630 let ind_w = (ind_end - cap_ofs).max(0.0);
2631
2632 if t > 0.0 && ind_w > 0.0 {
2634 scene.nodes.push(SceneNode::Rect {
2635 rect: Rect {
2636 x: rect.x + cap_ofs,
2637 y: cy - corner,
2638 w: ind_w,
2639 h: track_h,
2640 },
2641 brush: Brush::Solid(mul_c(config.color)),
2642 radius: [cap_radius; 4],
2643 });
2644 }
2645
2646 let track_start = (rect.x + ind_end + gap).min(rect.x + rect.w);
2648 let track_w = (rect.x + rect.w - track_start).max(0.0);
2649 if t < 1.0 && track_w > 0.0 {
2650 let track_left = track_start + cap_ofs;
2651 let track_right = rect.x + rect.w;
2652 if track_right > track_left {
2653 scene.nodes.push(SceneNode::Rect {
2654 rect: Rect {
2655 x: track_left,
2656 y: cy - corner,
2657 w: track_right - track_left,
2658 h: track_h,
2659 },
2660 brush: Brush::Solid(mul_c(config.track_color)),
2661 radius: [cap_radius; 4],
2662 });
2663 }
2664 }
2665
2666 {
2668 let sx = rect.x + rect.w - dot_r;
2669 scene.nodes.push(SceneNode::Ellipse {
2670 rect: Rect {
2671 x: sx - dot_r,
2672 y: cy - dot_r,
2673 w: dot_r * 2.0,
2674 h: dot_r * 2.0,
2675 },
2676 brush: Brush::Solid(mul_c(config.color)),
2677 });
2678 }
2679 }))
2680 .semantics(Semantics {
2681 role: Role::ProgressBar,
2682 label: None,
2683 focused: false,
2684 enabled: true,
2685 selectable_group: false,
2686 })
2687}
2688
2689#[allow(dead_code)]
2692#[derive(Clone, Debug)]
2693pub struct TextFieldColors {
2694 pub focused_text_color: Color,
2695 pub unfocused_text_color: Color,
2696 pub disabled_text_color: Color,
2697 pub error_text_color: Color,
2698 pub focused_container_color: Color,
2699 pub unfocused_container_color: Color,
2700 pub disabled_container_color: Color,
2701 pub error_container_color: Color,
2702 pub cursor_color: Color,
2703 pub error_cursor_color: Color,
2704 pub focused_indicator_color: Color,
2705 pub unfocused_indicator_color: Color,
2706 pub disabled_indicator_color: Color,
2707 pub error_indicator_color: Color,
2708 pub focused_leading_icon_color: Color,
2709 pub unfocused_leading_icon_color: Color,
2710 pub disabled_leading_icon_color: Color,
2711 pub error_leading_icon_color: Color,
2712 pub focused_trailing_icon_color: Color,
2713 pub unfocused_trailing_icon_color: Color,
2714 pub disabled_trailing_icon_color: Color,
2715 pub error_trailing_icon_color: Color,
2716 pub focused_label_color: Color,
2717 pub unfocused_label_color: Color,
2718 pub disabled_label_color: Color,
2719 pub error_label_color: Color,
2720 pub focused_placeholder_color: Color,
2721 pub unfocused_placeholder_color: Color,
2722 pub disabled_placeholder_color: Color,
2723 pub error_placeholder_color: Color,
2724 pub focused_supporting_text_color: Color,
2725 pub unfocused_supporting_text_color: Color,
2726 pub disabled_supporting_text_color: Color,
2727 pub error_supporting_text_color: Color,
2728 pub focused_prefix_color: Color,
2729 pub unfocused_prefix_color: Color,
2730 pub disabled_prefix_color: Color,
2731 pub error_prefix_color: Color,
2732 pub focused_suffix_color: Color,
2733 pub unfocused_suffix_color: Color,
2734 pub disabled_suffix_color: Color,
2735 pub error_suffix_color: Color,
2736}
2737
2738#[allow(dead_code)]
2739impl TextFieldColors {
2740 pub fn text_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2741 if !enabled {
2742 self.disabled_text_color
2743 } else if is_error {
2744 self.error_text_color
2745 } else if focused {
2746 self.focused_text_color
2747 } else {
2748 self.unfocused_text_color
2749 }
2750 }
2751 pub fn container_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2752 if !enabled {
2753 self.disabled_container_color
2754 } else if is_error {
2755 self.error_container_color
2756 } else if focused {
2757 self.focused_container_color
2758 } else {
2759 self.unfocused_container_color
2760 }
2761 }
2762 pub fn cursor_color(&self, is_error: bool) -> Color {
2763 if is_error {
2764 self.error_cursor_color
2765 } else {
2766 self.cursor_color
2767 }
2768 }
2769 pub fn indicator_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2770 if !enabled {
2771 self.disabled_indicator_color
2772 } else if is_error {
2773 self.error_indicator_color
2774 } else if focused {
2775 self.focused_indicator_color
2776 } else {
2777 self.unfocused_indicator_color
2778 }
2779 }
2780 pub fn leading_icon_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2781 if !enabled {
2782 self.disabled_leading_icon_color
2783 } else if is_error {
2784 self.error_leading_icon_color
2785 } else if focused {
2786 self.focused_leading_icon_color
2787 } else {
2788 self.unfocused_leading_icon_color
2789 }
2790 }
2791 pub fn trailing_icon_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2792 if !enabled {
2793 self.disabled_trailing_icon_color
2794 } else if is_error {
2795 self.error_trailing_icon_color
2796 } else if focused {
2797 self.focused_trailing_icon_color
2798 } else {
2799 self.unfocused_trailing_icon_color
2800 }
2801 }
2802 pub fn label_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2803 if !enabled {
2804 self.disabled_label_color
2805 } else if is_error {
2806 self.error_label_color
2807 } else if focused {
2808 self.focused_label_color
2809 } else {
2810 self.unfocused_label_color
2811 }
2812 }
2813 pub fn placeholder_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2814 if !enabled {
2815 self.disabled_placeholder_color
2816 } else if is_error {
2817 self.error_placeholder_color
2818 } else if focused {
2819 self.focused_placeholder_color
2820 } else {
2821 self.unfocused_placeholder_color
2822 }
2823 }
2824 pub fn supporting_text_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2825 if !enabled {
2826 self.disabled_supporting_text_color
2827 } else if is_error {
2828 self.error_supporting_text_color
2829 } else if focused {
2830 self.focused_supporting_text_color
2831 } else {
2832 self.unfocused_supporting_text_color
2833 }
2834 }
2835}
2836
2837pub struct TextFieldDefaults;
2839
2840impl TextFieldDefaults {
2841 pub const MIN_HEIGHT: f32 = 56.0;
2843 pub const MIN_WIDTH: f32 = 280.0;
2845
2846 pub fn colors() -> TextFieldColors {
2847 let th = theme();
2848 TextFieldColors {
2849 focused_text_color: th.on_surface,
2850 unfocused_text_color: th.on_surface,
2851 disabled_text_color: th.on_surface.with_alpha_f32(0.38),
2852 error_text_color: th.on_surface,
2853 focused_container_color: th.surface_container_highest,
2854 unfocused_container_color: th.surface_container_highest,
2855 disabled_container_color: th.on_surface.with_alpha_f32(0.04),
2856 error_container_color: th.surface_container_highest,
2857 cursor_color: th.primary,
2858 error_cursor_color: th.error,
2859 focused_indicator_color: th.primary,
2860 unfocused_indicator_color: th.on_surface_variant,
2861 disabled_indicator_color: th.on_surface.with_alpha_f32(0.12),
2862 error_indicator_color: th.error,
2863 focused_leading_icon_color: th.on_surface_variant,
2864 unfocused_leading_icon_color: th.on_surface_variant,
2865 disabled_leading_icon_color: th.on_surface.with_alpha_f32(0.38),
2866 error_leading_icon_color: th.error,
2867 focused_trailing_icon_color: th.on_surface_variant,
2868 unfocused_trailing_icon_color: th.on_surface_variant,
2869 disabled_trailing_icon_color: th.on_surface.with_alpha_f32(0.38),
2870 error_trailing_icon_color: th.error,
2871 focused_label_color: th.primary,
2872 unfocused_label_color: th.on_surface_variant,
2873 disabled_label_color: th.on_surface.with_alpha_f32(0.38),
2874 error_label_color: th.error,
2875 focused_placeholder_color: th.on_surface_variant,
2876 unfocused_placeholder_color: th.on_surface_variant,
2877 disabled_placeholder_color: th.on_surface.with_alpha_f32(0.38),
2878 error_placeholder_color: th.error,
2879 focused_supporting_text_color: th.on_surface_variant,
2880 unfocused_supporting_text_color: th.on_surface_variant,
2881 disabled_supporting_text_color: th.on_surface.with_alpha_f32(0.38),
2882 error_supporting_text_color: th.error,
2883 focused_prefix_color: th.on_surface,
2884 unfocused_prefix_color: th.on_surface,
2885 disabled_prefix_color: th.on_surface.with_alpha_f32(0.38),
2886 error_prefix_color: th.on_surface,
2887 focused_suffix_color: th.on_surface,
2888 unfocused_suffix_color: th.on_surface,
2889 disabled_suffix_color: th.on_surface.with_alpha_f32(0.38),
2890 error_suffix_color: th.on_surface,
2891 }
2892 }
2893}
2894
2895#[derive(Clone)]
2897pub struct OutlinedTextFieldConfig {
2898 pub label: Option<String>,
2902 pub placeholder: Option<String>,
2906 pub leading_icon: Option<View>,
2908 pub trailing_icon: Option<View>,
2910 pub single_line: bool,
2912 pub is_error: bool,
2914 pub enabled: bool,
2916 pub on_submit: Option<Rc<dyn Fn(String)>>,
2918 pub colors: Option<TextFieldColors>,
2920 pub focus_tracker: Option<Rc<Cell<bool>>>,
2924}
2925
2926impl Default for OutlinedTextFieldConfig {
2927 fn default() -> Self {
2928 Self {
2929 label: None,
2930 placeholder: None,
2931 leading_icon: None,
2932 trailing_icon: None,
2933 single_line: true,
2934 is_error: false,
2935 enabled: true,
2936 on_submit: None,
2937 colors: None,
2938 focus_tracker: None,
2939 }
2940 }
2941}
2942
2943pub fn OutlinedTextField(
2965 modifier: Modifier,
2966 value: String,
2967 on_value_change: impl Fn(String) + 'static,
2968 config: OutlinedTextFieldConfig,
2969) -> View {
2970 let label_str: Option<Rc<str>> = config.label.clone().map(Rc::from);
2971 let has_label = label_str.is_some();
2972
2973 let anim_key = match &label_str {
2975 Some(l) => format!("otf_{}", &l[..l.len().min(32)]),
2976 None => "otf_nolabel".into(),
2977 };
2978
2979 let focus_tracker: Rc<Cell<bool>> = match config.focus_tracker.clone() {
2983 Some(ft) => ft,
2984 None => remember_with_key(format!("otf_focus_{}", anim_key), || Cell::new(false)),
2985 };
2986 let is_focused = focus_tracker.get();
2987 let should_float = !value.is_empty() || is_focused;
2988
2989 let tf_placeholder = if has_label {
2990 if should_float {
2991 config.placeholder.clone().unwrap_or_default()
2992 } else {
2993 String::new()
2994 }
2995 } else {
2996 config.placeholder.clone().unwrap_or_default()
2997 };
2998
2999 let text_input = View::new(0, ViewKind::Box)
3000 .modifier(
3001 Modifier::new().flex_grow(1.0).text_input(TextInputConfig {
3002 hint: tf_placeholder,
3003 multiline: false,
3004 on_change: Some(Rc::new(on_value_change) as _),
3005 on_submit: config.on_submit.clone().map(|f| {
3006 let f = f.clone();
3007 Rc::new(move |s| f(s)) as Rc<dyn Fn(String)>
3008 }),
3009 focus_tracker: Some(focus_tracker),
3010 value: value.clone(),
3011 visual_transformation: None,
3012 keyboard_type: Default::default(),
3013 capitalization: Default::default(),
3014 ime_action: Default::default(),
3015 enabled: config.enabled,
3016 read_only: false,
3017 max_lines: None,
3018 min_lines: 1,
3019 cursor_color: config
3020 .colors
3021 .as_ref()
3022 .map(|c| c.cursor_color(config.is_error)),
3023 on_text_layout: None,
3024 text_style: None,
3025 keyboard_actions: None,
3026 interaction_source: None,
3027 line_limits: None,
3028 }),
3029 )
3030 .semantics(Semantics {
3031 role: Role::TextField,
3032 label: None,
3033 focused: false,
3034 enabled: true,
3035 selectable_group: false,
3036 });
3037
3038 outlined_field_decoration(
3039 modifier,
3040 anim_key,
3041 label_str,
3042 &config,
3043 is_focused,
3044 !value.is_empty(),
3045 text_input,
3046 )
3047}
3048
3049pub fn OutlinedTextFieldState(
3051 modifier: Modifier,
3052 state: Rc<RefCell<TextFieldState>>,
3053 on_value_change: impl Fn(String) + 'static,
3054 config: OutlinedTextFieldConfig,
3055) -> View {
3056 let label_str: Option<Rc<str>> = config.label.clone().map(Rc::from);
3057 let has_label = label_str.is_some();
3058
3059 let anim_key = match &label_str {
3061 Some(l) => format!("otf_{}", &l[..l.len().min(32)]),
3062 None => "otf_nolabel".into(),
3063 };
3064
3065 let focus_tracker: Rc<Cell<bool>> = match config.focus_tracker.clone() {
3066 Some(ft) => ft,
3067 None => remember_with_key(format!("otf_focus_{}", anim_key), || Cell::new(false)),
3068 };
3069 let is_focused = focus_tracker.get();
3070 let has_content = !state.borrow().text.is_empty();
3071 let should_float = has_content || is_focused;
3072
3073 let tf_placeholder = if has_label {
3075 if should_float {
3076 config.placeholder.clone().unwrap_or_default()
3077 } else {
3078 String::new()
3079 }
3080 } else {
3081 config.placeholder.clone().unwrap_or_default()
3082 };
3083
3084 let text_input = BasicTextField(
3085 state,
3086 Modifier::new().flex_grow(1.0),
3087 tf_placeholder,
3088 BasicTextFieldConfig {
3089 line_limits: if config.single_line {
3090 TextFieldLineLimits::SingleLine
3091 } else {
3092 TextFieldLineLimits::MultiLine {
3093 min_height_in_lines: 1,
3094 max_height_in_lines: usize::MAX,
3095 }
3096 },
3097 on_change: Some(Rc::new(on_value_change)),
3098 on_submit: config.on_submit.clone(),
3099 focus_tracker: Some(focus_tracker),
3100 enabled: config.enabled,
3101 ..Default::default()
3102 },
3103 );
3104
3105 outlined_field_decoration(
3106 modifier,
3107 anim_key,
3108 label_str,
3109 &config,
3110 is_focused,
3111 has_content,
3112 text_input,
3113 )
3114}
3115
3116fn outlined_field_decoration(
3117 modifier: Modifier,
3118 anim_key: String,
3119 label_str: Option<Rc<str>>,
3120 config: &OutlinedTextFieldConfig,
3121 is_focused: bool,
3122 has_content: bool,
3123 text_input: View,
3124) -> View {
3125 let th = theme();
3126 let has_label = label_str.is_some();
3127
3128 let should_float = has_content || is_focused;
3129 let float_t = animate_f32(
3130 anim_key.clone(),
3131 if should_float { 1.0 } else { 0.0 },
3132 th.motion.color,
3133 );
3134
3135 let target_border_w = if config.is_error || should_float {
3136 OutlinedTextFieldDefaults::FOCUSED_BORDER_THICKNESS
3137 } else {
3138 OutlinedTextFieldDefaults::UNFOCUSED_BORDER_THICKNESS
3139 };
3140 let border_w = animate_f32(
3141 format!("otf_bw_{}", anim_key),
3142 target_border_w,
3143 th.motion.color,
3144 );
3145
3146 let (border_color, label_color, container_bg) = if let Some(ref tc) = config.colors {
3147 (
3148 tc.indicator_color(config.enabled, config.is_error, is_focused),
3149 tc.label_color(config.enabled, config.is_error, is_focused),
3150 tc.container_color(config.enabled, config.is_error, is_focused),
3151 )
3152 } else {
3153 (
3154 if config.is_error {
3155 th.error
3156 } else if is_focused {
3157 th.primary
3158 } else {
3159 th.outline
3160 },
3161 if config.is_error {
3162 th.error
3163 } else if is_focused {
3164 th.primary
3165 } else {
3166 th.on_surface_variant
3167 },
3168 th.surface,
3169 )
3170 };
3171
3172 let label_size = 16.0 - 4.0 * float_t;
3174
3175 let min_label_half_h: f32 = if has_label { 8.0 } else { 0.0 };
3177
3178 let label_start_y = (56.0 - 16.0) / 2.0;
3180 let label_end_y = -min_label_half_h;
3181 let label_y = label_start_y - (label_start_y - label_end_y) * float_t;
3182
3183 let label_start_x = if has_label { 24.0 } else { 0.0 };
3185 let label_end_x = if has_label { 20.0 } else { 0.0 };
3186 let label_x = label_start_x - (label_start_x - label_end_x) * float_t;
3187
3188 let (top_pad, bottom_pad) = if has_label { (8.0, 8.0) } else { (16.0, 16.0) };
3190
3191 let label_cutout = label_str.as_ref().map(|lbl| {
3194 let font_px = dp_to_px(label_size) * repose_core::locals::text_scale().0;
3195 let m = measure_text(lbl, font_px, TextMeasureConfig::default());
3196 let text_width_px = m.positions.last().copied().unwrap_or(0.0);
3197 let text_width_dp = px_to_dp(text_width_px);
3198 let pad = 1.0;
3199 let line_h = 16.0;
3200 (
3201 label_x - pad,
3202 label_y - pad,
3203 label_x + text_width_dp + pad,
3204 label_y + line_h + pad,
3205 )
3206 });
3207
3208 ZStack(
3209 modifier
3210 .min_height(OutlinedTextFieldDefaults::MIN_HEIGHT)
3211 .min_width(OutlinedTextFieldDefaults::MIN_WIDTH),
3212 )
3213 .child((
3214 Box(Modifier::new()
3216 .fill_max_size()
3217 .clip_rounded(th.shapes.small)
3218 .background(container_bg)),
3219 if has_label {
3221 let mut bm = Modifier::new()
3222 .fill_max_size()
3223 .clip_rounded(th.shapes.small)
3224 .border(border_w, border_color, th.shapes.small);
3225 if let Some((l, t, r, b)) = label_cutout {
3226 bm = bm.clip_rect(l, t, r, b, ClipOp::Difference);
3227 }
3228 Box(bm)
3229 } else {
3230 Box(Modifier::new()
3231 .fill_max_size()
3232 .clip_rounded(th.shapes.small)
3233 .border(border_w, border_color, th.shapes.small))
3234 },
3235 Row(Modifier::new()
3237 .fill_max_size()
3238 .padding_values(PaddingValues {
3239 left: 16.0,
3240 right: 16.0,
3241 top: top_pad,
3242 bottom: bottom_pad,
3243 })
3244 .align_items(AlignItems::CENTER))
3245 .child((
3246 config.leading_icon.clone().unwrap_or(Box(Modifier::new())),
3247 text_input,
3248 config.trailing_icon.clone().unwrap_or(Box(Modifier::new())),
3249 )),
3250 if let Some(lbl) = label_str {
3252 Box(Modifier::new()
3253 .min_width(200.0)
3254 .padding_values(PaddingValues {
3255 left: label_x,
3256 right: 20.0,
3257 top: 0.0,
3258 bottom: 0.0,
3259 })
3260 .absolute()
3261 .offset(Some(0.0), Some(label_y), None, None))
3262 .child(
3263 Text(lbl.as_ref().to_string())
3264 .color(label_color)
3265 .size(label_size),
3266 )
3267 } else {
3268 Box(Modifier::new())
3269 },
3270 ))
3271}
3272
3273#[derive(Clone)]
3275pub struct TextFieldConfig {
3276 pub label: Option<String>,
3277 pub placeholder: Option<String>,
3278 pub leading_icon: Option<View>,
3279 pub trailing_icon: Option<View>,
3280 pub single_line: bool,
3281 pub is_error: bool,
3282 pub enabled: bool,
3283 pub on_submit: Option<Rc<dyn Fn(String)>>,
3284 pub colors: Option<TextFieldColors>,
3285}
3286
3287impl Default for TextFieldConfig {
3288 fn default() -> Self {
3289 Self {
3290 label: None,
3291 placeholder: None,
3292 leading_icon: None,
3293 trailing_icon: None,
3294 single_line: true,
3295 is_error: false,
3296 enabled: true,
3297 on_submit: None,
3298 colors: None,
3299 }
3300 }
3301}
3302
3303pub fn TextField(
3310 modifier: Modifier,
3311 value: String,
3312 on_value_change: impl Fn(String) + 'static,
3313 config: TextFieldConfig,
3314) -> View {
3315 let th = theme();
3316 let label_str: Option<Rc<str>> = config.label.map(Rc::from);
3317 let has_label = label_str.is_some();
3318
3319 let anim_key = match &label_str {
3320 Some(l) => format!("tf_{}", &l[..l.len().min(32)]),
3321 None => "tf_nolabel".into(),
3322 };
3323
3324 let focus_tracker: Rc<Cell<bool>> =
3325 remember_with_key(format!("tf_focus_{}", anim_key), || Cell::new(false));
3326 let is_focused = focus_tracker.get();
3327 let should_float = !value.is_empty() || is_focused;
3328
3329 let float_t = animate_f32(
3330 anim_key.clone(),
3331 if should_float { 1.0 } else { 0.0 },
3332 th.motion.color,
3333 );
3334
3335 let (indicator_color, label_color, container_bg) = if let Some(ref tc) = config.colors {
3336 let enf = config.enabled && is_focused;
3337 let ind = tc.indicator_color(config.enabled, config.is_error, enf);
3338 let lb = tc.label_color(config.enabled, config.is_error, enf);
3339 let bg = tc.container_color(config.enabled, config.is_error, enf);
3340 (ind, lb, bg)
3341 } else {
3342 let ind = 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 lb = if config.is_error {
3350 th.error
3351 } else if float_t > 0.5 {
3352 th.primary
3353 } else {
3354 th.on_surface_variant
3355 };
3356 let bg = if config.enabled {
3357 th.surface_container_highest
3358 } else {
3359 th.on_surface
3360 .with_alpha_f32(0.04)
3361 .composite_over(th.surface)
3362 };
3363 (ind, lb, bg)
3364 };
3365
3366 let label_size = 16.0 - 4.0 * float_t;
3367
3368 let label_start_y = (56.0 - 16.0) / 2.0;
3369 let label_end_y = if has_label { 8.0 } else { 0.0 };
3370 let label_y = label_start_y - (label_start_y - label_end_y) * float_t;
3371
3372 let label_start_x = if has_label { 24.0 } else { 0.0 };
3373 let label_end_x = if has_label { 20.0 } else { 0.0 };
3374 let label_x = label_start_x - (label_start_x - label_end_x) * float_t;
3375
3376 let tf_placeholder = if has_label {
3377 if should_float {
3378 config.placeholder.unwrap_or_default()
3379 } else {
3380 String::new()
3381 }
3382 } else {
3383 config.placeholder.unwrap_or_default()
3384 };
3385
3386 let indicator_active = config.is_error || (config.enabled && is_focused);
3387 let indicator_target_w = if indicator_active { 2.0 } else { 1.0 };
3388 let indicator_w = animate_f32(
3389 format!("tf_ind_w_{}", anim_key),
3390 indicator_target_w,
3391 th.motion.color,
3392 );
3393
3394 let (top_pad, bottom_pad) = if has_label { (8.0, 8.0) } else { (16.0, 16.0) };
3395
3396 Column(
3397 modifier
3398 .min_height(TextFieldDefaults::MIN_HEIGHT)
3399 .min_width(TextFieldDefaults::MIN_WIDTH),
3400 )
3401 .child((
3402 Box(Modifier::new()
3404 .fill_max_size()
3405 .clip_rounded(th.shapes.extra_small)
3406 .background(container_bg))
3407 .child(
3408 Column(Modifier::new().fill_max_size()).child((
3409 Row(Modifier::new()
3411 .fill_max_size()
3412 .padding_values(PaddingValues {
3413 left: 16.0,
3414 right: 16.0,
3415 top: top_pad,
3416 bottom: bottom_pad,
3417 })
3418 .align_items(AlignItems::CENTER))
3419 .child((
3420 config.leading_icon.unwrap_or(Box(Modifier::new())),
3421 View::new(0, ViewKind::Box)
3422 .modifier(
3423 Modifier::new().flex_grow(1.0).text_input(TextInputConfig {
3424 hint: tf_placeholder,
3425 multiline: !config.single_line,
3426 on_change: Some(Rc::new(on_value_change) as _),
3427 on_submit: config.on_submit.clone().map(|f| {
3428 let f = f.clone();
3429 Rc::new(move |s| f(s)) as Rc<dyn Fn(String)>
3430 }),
3431 focus_tracker: Some(focus_tracker.clone()),
3432 value: value.clone(),
3433 visual_transformation: None,
3434 keyboard_type: Default::default(),
3435 capitalization: Default::default(),
3436 ime_action: Default::default(),
3437 enabled: config.enabled,
3438 read_only: false,
3439 max_lines: None,
3440 min_lines: 1,
3441 cursor_color: config
3442 .colors
3443 .as_ref()
3444 .map(|c| c.cursor_color(config.is_error)),
3445 on_text_layout: None,
3446 text_style: None,
3447 keyboard_actions: None,
3448 interaction_source: None,
3449 line_limits: None,
3450 }),
3451 )
3452 .semantics(Semantics {
3453 role: Role::TextField,
3454 label: None,
3455 focused: false,
3456 enabled: true,
3457 selectable_group: false,
3458 }),
3459 config.trailing_icon.unwrap_or(Box(Modifier::new())),
3460 )),
3461 Box(Modifier::new()
3463 .fill_max_width()
3464 .height(indicator_w)
3465 .absolute()
3466 .offset(None, None, None, Some(0.0))
3467 .background(indicator_color)),
3468 )),
3469 ),
3470 if let Some(lbl) = label_str {
3472 Box(Modifier::new()
3473 .min_width(200.0)
3474 .padding_values(PaddingValues {
3475 left: label_x,
3476 right: 20.0,
3477 top: 0.0,
3478 bottom: 0.0,
3479 })
3480 .absolute()
3481 .offset(Some(0.0), Some(label_y), None, None))
3482 .child(
3483 Text(lbl.as_ref().to_string())
3484 .color(label_color)
3485 .size(label_size),
3486 )
3487 } else {
3488 Box(Modifier::new())
3489 },
3490 ))
3491}
3492
3493#[derive(Clone, Debug)]
3495pub struct CheckboxConfig {
3496 pub modifier: Modifier,
3497 pub enabled: bool,
3499 pub checked_color: Color,
3500 pub unchecked_color: Color,
3501 pub checkmark_color: Color,
3502 pub checked_border_color: Color,
3504 pub unchecked_border_color: Color,
3506 pub disabled_checked_box_color: Color,
3507 pub disabled_unchecked_box_color: Color,
3508 pub disabled_indeterminate_box_color: Color,
3509 pub disabled_checkmark_color: Color,
3510 pub disabled_checked_border_color: Color,
3511 pub disabled_unchecked_border_color: Color,
3512 pub disabled_indeterminate_border_color: Color,
3513 pub state_colors: StateColors,
3514 pub interaction_source: Option<MutableInteractionSource>,
3515}
3516
3517impl Default for CheckboxConfig {
3518 fn default() -> Self {
3519 Self {
3520 modifier: Modifier::new(),
3521 enabled: true,
3522 checked_color: CheckboxDefaults::checked_color(),
3523 unchecked_color: CheckboxDefaults::unchecked_color(),
3524 checkmark_color: CheckboxDefaults::checkmark_color(),
3525 checked_border_color: CheckboxDefaults::checked_color(),
3526 unchecked_border_color: CheckboxDefaults::unchecked_color(),
3527 disabled_checked_box_color: CheckboxDefaults::disabled_checked_box_color(),
3528 disabled_unchecked_box_color: Color::TRANSPARENT,
3529 disabled_indeterminate_box_color: CheckboxDefaults::disabled_checked_box_color(),
3530 disabled_checkmark_color: CheckboxDefaults::disabled_checkmark_color(),
3531 disabled_checked_border_color: CheckboxDefaults::disabled_checked_box_color(),
3532 disabled_unchecked_border_color: CheckboxDefaults::disabled_unchecked_border_color(),
3533 disabled_indeterminate_border_color: CheckboxDefaults::disabled_checked_box_color(),
3534 state_colors: CheckboxDefaults::state_colors_default(),
3535 interaction_source: None,
3536 }
3537 }
3538}
3539
3540static CHECKBOX_COUNTER: AtomicU64 = AtomicU64::new(0);
3544pub fn Checkbox(checked: bool, on_change: impl Fn(bool) + 'static, config: CheckboxConfig) -> View {
3545 let th = theme();
3546 let sz = CheckboxDefaults::BOX_SIZE;
3547
3548 let id = remember(|| CHECKBOX_COUNTER.fetch_add(1, Ordering::Relaxed));
3549 let spec = th.motion.color_fast;
3550
3551 let is_enabled = config.enabled;
3552
3553 let fill = animate_color(
3554 format!("cb_fill_{}", id),
3555 if !is_enabled {
3556 if checked {
3557 config.disabled_checked_box_color
3558 } else {
3559 config.disabled_unchecked_box_color
3560 }
3561 } else if checked {
3562 config.checked_color
3563 } else {
3564 Color::TRANSPARENT
3565 },
3566 spec,
3567 );
3568 let bd_w = animate_f32(
3569 format!("cb_bw_{}", id),
3570 if !is_enabled && checked {
3571 0.0
3572 } else if !is_enabled {
3573 CheckboxDefaults::STROKE_WIDTH
3574 } else if checked {
3575 0.0
3576 } else {
3577 CheckboxDefaults::STROKE_WIDTH
3578 },
3579 spec,
3580 );
3581 let bd = animate_color(
3582 format!("cb_bd_{}", id),
3583 if !is_enabled {
3584 if checked {
3585 config.disabled_checked_border_color
3586 } else {
3587 config.disabled_unchecked_border_color
3588 }
3589 } else if checked {
3590 Color::TRANSPARENT
3591 } else {
3592 config.unchecked_border_color
3593 },
3594 spec,
3595 );
3596 let check_alpha = animate_f32(
3597 format!("cb_ca_{}", id),
3598 if checked { 1.0 } else { 0.0 },
3599 spec,
3600 );
3601 let check_col = if !is_enabled {
3602 config.disabled_checkmark_color
3603 } else {
3604 config.checkmark_color
3605 };
3606
3607 let cb = move || {
3608 if config.enabled {
3609 on_change(!checked)
3610 }
3611 };
3612
3613 let cb_source: Rc<MutableInteractionSource> = config
3614 .interaction_source
3615 .clone()
3616 .map(Rc::new)
3617 .unwrap_or_else(|| remember(MutableInteractionSource::new));
3618 Box(Modifier::new()
3619 .width(CheckboxDefaults::TOUCH_TARGET_SIZE)
3620 .height(CheckboxDefaults::TOUCH_TARGET_SIZE)
3621 .padding(0.0)
3622 .clip_rounded(20.0)
3623 .background(Color::TRANSPARENT)
3624 .state_colors(config.state_colors)
3625 .interaction_source(&*cb_source)
3626 .clickable()
3627 .align_items(AlignItems::CENTER)
3628 .justify_content(JustifyContent::CENTER)
3629 .on_click(cb)
3630 .then(config.modifier))
3631 .child(
3632 Box(Modifier::new()
3633 .size(sz, sz)
3634 .background(fill)
3635 .border(bd_w, bd, CheckboxDefaults::CORNER_RADIUS)
3636 .clip_rounded(CheckboxDefaults::CORNER_RADIUS)
3637 .align_items(AlignItems::CENTER)
3638 .justify_content(JustifyContent::CENTER))
3639 .child(if check_alpha > 0.01 {
3640 Box(Modifier::new().alpha(check_alpha)).child(
3641 Icon(Symbol::new("done", '\u{E876}'))
3642 .color(check_col)
3643 .size(CheckboxDefaults::CHECK_ICON_SIZE),
3644 )
3645 } else {
3646 Box(Modifier::new())
3647 }),
3648 )
3649}
3650
3651#[derive(Clone, Copy, Debug, PartialEq)]
3653pub enum TriState {
3654 Checked,
3655 Unchecked,
3656 Indeterminate,
3657}
3658
3659pub fn TriStateCheckbox(
3662 state: TriState,
3663 on_change: impl Fn(TriState) + 'static,
3664 config: CheckboxConfig,
3665) -> View {
3666 let th = theme();
3667 let sz = CheckboxDefaults::BOX_SIZE;
3668
3669 let id = remember(|| CHECKBOX_COUNTER.fetch_add(1, Ordering::Relaxed));
3670 let spec = th.motion.color_fast;
3671
3672 let is_checked = state == TriState::Checked;
3673 let is_indeterminate = state == TriState::Indeterminate;
3674 let has_fill = is_checked || is_indeterminate;
3675 let is_enabled = config.enabled;
3676
3677 let fill = animate_color(
3678 format!("tc_fill_{}", id),
3679 if !is_enabled {
3680 if has_fill {
3681 config.disabled_indeterminate_box_color
3682 } else {
3683 config.disabled_unchecked_box_color
3684 }
3685 } else if has_fill {
3686 config.checked_color
3687 } else {
3688 Color::TRANSPARENT
3689 },
3690 spec,
3691 );
3692 let bd_w = animate_f32(
3693 format!("tc_bw_{}", id),
3694 if !is_enabled {
3695 if has_fill {
3696 0.0
3697 } else {
3698 CheckboxDefaults::STROKE_WIDTH
3699 }
3700 } else if has_fill {
3701 0.0
3702 } else {
3703 CheckboxDefaults::STROKE_WIDTH
3704 },
3705 spec,
3706 );
3707 let bd = animate_color(
3708 format!("tc_bd_{}", id),
3709 if !is_enabled {
3710 if has_fill {
3711 config.disabled_indeterminate_border_color
3712 } else {
3713 config.disabled_unchecked_border_color
3714 }
3715 } else if has_fill {
3716 Color::TRANSPARENT
3717 } else {
3718 config.unchecked_border_color
3719 },
3720 spec,
3721 );
3722 let symbol_alpha = animate_f32(
3723 format!("tc_sa_{}", id),
3724 if has_fill { 1.0 } else { 0.0 },
3725 spec,
3726 );
3727 let symbol_col = if !is_enabled {
3728 config.disabled_checkmark_color
3729 } else {
3730 config.checkmark_color
3731 };
3732
3733 Box(Modifier::new()
3734 .width(CheckboxDefaults::TOUCH_TARGET_SIZE)
3735 .height(CheckboxDefaults::TOUCH_TARGET_SIZE)
3736 .padding(0.0)
3737 .clip_rounded(20.0)
3738 .background(Color::TRANSPARENT)
3739 .clickable()
3740 .align_items(AlignItems::CENTER)
3741 .justify_content(JustifyContent::CENTER)
3742 .on_click(move || {
3743 if is_enabled {
3744 on_change(match state {
3745 TriState::Checked => TriState::Unchecked,
3746 TriState::Indeterminate => TriState::Checked,
3747 TriState::Unchecked => TriState::Checked,
3748 })
3749 }
3750 })
3751 .then(config.modifier))
3752 .child(
3753 Box(Modifier::new()
3754 .size(sz, sz)
3755 .background(fill)
3756 .border(bd_w, bd, CheckboxDefaults::CORNER_RADIUS)
3757 .clip_rounded(CheckboxDefaults::CORNER_RADIUS)
3758 .align_items(AlignItems::CENTER)
3759 .justify_content(JustifyContent::CENTER))
3760 .child(if symbol_alpha > 0.01 {
3761 Box(Modifier::new().alpha(symbol_alpha)).child(if is_indeterminate {
3762 Box(Modifier::new()
3764 .width(10.0)
3765 .height(2.0)
3766 .background(symbol_col)
3767 .clip_rounded(1.0))
3768 } else {
3769 Icon(Symbol::new("done", '\u{E876}'))
3770 .color(symbol_col)
3771 .size(CheckboxDefaults::CHECK_ICON_SIZE)
3772 })
3773 } else {
3774 Box(Modifier::new())
3775 }),
3776 )
3777}
3778
3779#[derive(Clone, Debug)]
3781pub struct RadioButtonConfig {
3782 pub modifier: Modifier,
3783 pub enabled: bool,
3785 pub selected_color: Color,
3786 pub unselected_color: Color,
3787 pub disabled_selected_color: Color,
3788 pub disabled_unselected_color: Color,
3789 pub state_colors: StateColors,
3790 pub interaction_source: Option<MutableInteractionSource>,
3791}
3792
3793impl Default for RadioButtonConfig {
3794 fn default() -> Self {
3795 Self {
3796 modifier: Modifier::new(),
3797 enabled: true,
3798 selected_color: RadioButtonDefaults::selected_color(),
3799 unselected_color: RadioButtonDefaults::unselected_color(),
3800 disabled_selected_color: RadioButtonDefaults::disabled_selected_color(),
3801 disabled_unselected_color: RadioButtonDefaults::disabled_unselected_color(),
3802 state_colors: RadioButtonDefaults::state_colors_default(),
3803 interaction_source: None,
3804 }
3805 }
3806}
3807
3808static RADIO_COUNTER: AtomicU64 = AtomicU64::new(0);
3812pub fn RadioButton(
3813 selected: bool,
3814 on_select: impl Fn() + 'static,
3815 config: RadioButtonConfig,
3816) -> View {
3817 let th = theme();
3818 let d = RadioButtonDefaults::OUTER_RADIUS * 2.0;
3819
3820 let id = remember(|| RADIO_COUNTER.fetch_add(1, Ordering::Relaxed));
3821 let color_spec = th.motion.color_fast;
3822 let spring = th.motion.spring;
3823
3824 let ring_col = animate_color(
3825 format!("rb_ring_{}", id),
3826 if !config.enabled {
3827 if selected {
3828 config.disabled_selected_color
3829 } else {
3830 config.disabled_unselected_color
3831 }
3832 } else if selected {
3833 config.selected_color
3834 } else {
3835 config.unselected_color
3836 },
3837 color_spec,
3838 );
3839 let dot_size = animate_f32(
3840 format!("rb_dot_{}", id),
3841 if selected {
3842 RadioButtonDefaults::DOT_RADIUS * 2.0
3843 } else {
3844 0.0
3845 },
3846 spring,
3847 );
3848 let dot_col = if !config.enabled {
3849 config.disabled_selected_color
3850 } else {
3851 config.selected_color
3852 };
3853
3854 let cb = move || {
3855 if config.enabled {
3856 on_select()
3857 }
3858 };
3859
3860 let rb_source: Rc<MutableInteractionSource> = config
3861 .interaction_source
3862 .clone()
3863 .map(Rc::new)
3864 .unwrap_or_else(|| remember(MutableInteractionSource::new));
3865 Box(Modifier::new()
3866 .width(RadioButtonDefaults::TOUCH_TARGET_SIZE)
3867 .height(RadioButtonDefaults::TOUCH_TARGET_SIZE)
3868 .padding(0.0)
3869 .clip_rounded(20.0)
3870 .background(Color::TRANSPARENT)
3871 .state_colors(config.state_colors)
3872 .interaction_source(&*rb_source)
3873 .clickable()
3874 .align_items(AlignItems::CENTER)
3875 .justify_content(JustifyContent::CENTER)
3876 .on_click(cb)
3877 .then(config.modifier))
3878 .child(
3879 Box(Modifier::new()
3880 .size(d, d)
3881 .border(RadioButtonDefaults::STROKE_WIDTH, ring_col, d * 0.5)
3882 .clip_rounded(d * 0.5)
3883 .align_items(AlignItems::CENTER)
3884 .justify_content(JustifyContent::CENTER))
3885 .child(if dot_size > 0.5 {
3886 Box(Modifier::new()
3887 .size(dot_size, dot_size)
3888 .background(dot_col)
3889 .clip_rounded(dot_size * 0.5))
3890 } else {
3891 Box(Modifier::new())
3892 }),
3893 )
3894}
3895
3896#[derive(Clone, Debug)]
3898pub struct SwitchConfig {
3899 pub modifier: Modifier,
3900 pub enabled: bool,
3902 pub checked_track_color: Color,
3903 pub unchecked_track_color: Color,
3904 pub checked_thumb_color: Color,
3905 pub unchecked_thumb_color: Color,
3906 pub checked_icon_color: Color,
3908 pub unchecked_icon_color: Color,
3910 pub checked_border_color: Color,
3912 pub unchecked_border_color: Color,
3914 pub disabled_checked_thumb_color: Color,
3915 pub disabled_checked_track_color: Color,
3916 pub disabled_checked_border_color: Color,
3917 pub disabled_checked_icon_color: Color,
3918 pub disabled_unchecked_thumb_color: Color,
3919 pub disabled_unchecked_track_color: Color,
3920 pub disabled_unchecked_border_color: Color,
3921 pub disabled_unchecked_icon_color: Color,
3922 pub state_colors: StateColors,
3923 pub thumb_content: Option<View>,
3924 pub interaction_source: Option<MutableInteractionSource>,
3925}
3926
3927impl Default for SwitchConfig {
3928 fn default() -> Self {
3929 Self {
3930 modifier: Modifier::new(),
3931 enabled: true,
3932 checked_track_color: SwitchDefaults::checked_track_color(),
3933 unchecked_track_color: SwitchDefaults::unchecked_track_color(),
3934 checked_thumb_color: SwitchDefaults::checked_thumb_color(),
3935 unchecked_thumb_color: SwitchDefaults::unchecked_thumb_color(),
3936 checked_icon_color: SwitchDefaults::checked_icon_color(),
3937 unchecked_icon_color: SwitchDefaults::unchecked_icon_color(),
3938 checked_border_color: Color::TRANSPARENT,
3939 unchecked_border_color: SwitchDefaults::unchecked_border_color(),
3940 disabled_checked_thumb_color: SwitchDefaults::disabled_checked_thumb_color(),
3941 disabled_checked_track_color: SwitchDefaults::disabled_checked_track_color(),
3942 disabled_checked_border_color: Color::TRANSPARENT,
3943 disabled_checked_icon_color: SwitchDefaults::disabled_checked_icon_color(),
3944 disabled_unchecked_thumb_color: SwitchDefaults::disabled_unchecked_thumb_color(),
3945 disabled_unchecked_track_color: SwitchDefaults::disabled_unchecked_track_color(),
3946 disabled_unchecked_border_color: SwitchDefaults::disabled_unchecked_border_color(),
3947 disabled_unchecked_icon_color: SwitchDefaults::disabled_unchecked_icon_color(),
3948 state_colors: SwitchDefaults::state_colors_default(),
3949 thumb_content: None,
3950 interaction_source: None,
3951 }
3952 }
3953}
3954
3955static SWITCH_COUNTER: AtomicU64 = AtomicU64::new(0);
3959pub fn Switch(checked: bool, on_change: impl Fn(bool) + 'static, config: SwitchConfig) -> View {
3960 let th = theme();
3961 let track_w = SwitchDefaults::TRACK_WIDTH;
3962 let track_h = SwitchDefaults::TRACK_HEIGHT;
3963
3964 let id = remember(|| SWITCH_COUNTER.fetch_add(1, Ordering::Relaxed));
3965
3966 let hovered = remember(|| Signal::new(false));
3967 let pressed = remember(|| Signal::new(false));
3968
3969 let thumb_target_pos = if checked {
3971 track_w - SwitchDefaults::THUMB_CHECKED_SIZE - 4.0
3972 } else {
3973 8.0
3974 };
3975 let thumb_target_d = if checked {
3976 SwitchDefaults::THUMB_CHECKED_SIZE
3977 } else {
3978 SwitchDefaults::THUMB_UNCHECKED_SIZE
3979 };
3980 let spring = th.motion.spring;
3981
3982 let thumb_left = animate_f32(format!("sw_pos_{}", id), thumb_target_pos, spring);
3983 let thumb_d = animate_f32(format!("sw_d_{}", id), thumb_target_d, spring);
3984 let thumb_top = (track_h - thumb_d) * 0.5;
3985
3986 let color_spec = th.motion.color_fast;
3987 let is_enabled = config.enabled;
3988
3989 let track_bg = animate_color(
3990 format!("sw_tbg_{}", id),
3991 if !is_enabled {
3992 if checked {
3993 config.disabled_checked_track_color
3994 } else {
3995 config.disabled_unchecked_track_color
3996 }
3997 } else if checked {
3998 config.checked_track_color
3999 } else {
4000 config.unchecked_track_color
4001 },
4002 color_spec,
4003 );
4004 let thumb_bg = animate_color(
4005 format!("sw_tmbg_{}", id),
4006 if !is_enabled {
4007 if checked {
4008 config.disabled_checked_thumb_color
4009 } else {
4010 config.disabled_unchecked_thumb_color
4011 }
4012 } else if checked {
4013 config.checked_thumb_color
4014 } else {
4015 config.unchecked_thumb_color
4016 },
4017 color_spec,
4018 );
4019 let track_border = animate_f32(
4020 format!("sw_tb_{}", id),
4021 if !is_enabled {
4022 if checked { 0.0 } else { 2.0 }
4023 } else if checked {
4024 0.0
4025 } else {
4026 2.0
4027 },
4028 color_spec,
4029 );
4030 let border_color = animate_color(
4031 format!("sw_bc_{}", id),
4032 if !is_enabled {
4033 if checked {
4034 config.disabled_checked_border_color
4035 } else {
4036 config.disabled_unchecked_border_color
4037 }
4038 } else if checked {
4039 config.checked_border_color
4040 } else {
4041 config.unchecked_border_color
4042 },
4043 color_spec,
4044 );
4045
4046 let state_overlay = animate_color(
4047 format!("sw_ol_{}", id),
4048 if !is_enabled {
4049 Color::TRANSPARENT
4050 } else if pressed.get() {
4051 config.state_colors.pressed
4052 } else if hovered.get() {
4053 config.state_colors.hovered
4054 } else {
4055 config.state_colors.default
4056 },
4057 color_spec,
4058 );
4059
4060 let sw_source: Rc<MutableInteractionSource> = config
4061 .interaction_source
4062 .clone()
4063 .map(Rc::new)
4064 .unwrap_or_else(|| remember(MutableInteractionSource::new));
4065 Box(Modifier::new()
4066 .size(track_w, track_h)
4067 .padding(0.0)
4068 .clip_rounded(track_h * 0.5)
4069 .background(track_bg)
4070 .border(track_border, border_color, track_h * 0.5)
4071 .interaction_source(&*sw_source)
4072 .clickable()
4073 .on_pointer_enter({
4074 let h = hovered.clone();
4075 move |_| h.set(true)
4076 })
4077 .on_pointer_leave({
4078 let h = hovered.clone();
4079 let p = pressed.clone();
4080 move |_| {
4081 h.set(false);
4082 p.set(false);
4083 }
4084 })
4085 .on_pointer_down({
4086 let p = pressed.clone();
4087 move |_| p.set(true)
4088 })
4089 .on_click({
4090 let cb = on_change;
4091 move || cb(!checked)
4092 })
4093 .on_pointer_up({
4094 let p = pressed.clone();
4095 move |_| p.set(false)
4096 })
4097 .then(config.modifier))
4098 .child((
4099 Box(Modifier::new()
4100 .size(thumb_d, thumb_d)
4101 .background(thumb_bg)
4102 .clip_rounded(thumb_d * 0.5)
4103 .hit_passthrough()
4104 .absolute()
4105 .offset(Some(thumb_left), Some(thumb_top), None, None)),
4106 Box(Modifier::new()
4107 .size(40.0, 40.0)
4108 .clip_rounded(20.0)
4109 .background(state_overlay)
4110 .hit_passthrough()
4111 .absolute()
4112 .offset(
4113 Some(thumb_left + thumb_d * 0.5 - 20.0),
4114 Some(track_h * 0.5 - 20.0),
4115 None,
4116 None,
4117 )),
4118 ))
4119}
4120
4121#[derive(Clone)]
4123pub struct SliderConfig {
4124 pub modifier: Modifier,
4126 pub enabled: bool,
4128 pub active_track_color: Color,
4129 pub inactive_track_color: Color,
4130 pub thumb_color: Color,
4131 pub active_tick_color: Color,
4132 pub inactive_tick_color: Color,
4133 pub disabled_thumb_color: Color,
4134 pub disabled_active_track_color: Color,
4135 pub disabled_inactive_track_color: Color,
4136 pub disabled_active_tick_color: Color,
4137 pub disabled_inactive_tick_color: Color,
4138 pub state_colors: StateColors,
4139 pub on_value_change_finished: Option<Rc<dyn Fn()>>,
4140 pub interaction_source: Option<MutableInteractionSource>,
4141}
4142
4143impl std::fmt::Debug for SliderConfig {
4144 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4145 f.debug_struct("SliderConfig")
4146 .field("modifier", &self.modifier)
4147 .field("enabled", &self.enabled)
4148 .field("active_track_color", &self.active_track_color)
4149 .field("inactive_track_color", &self.inactive_track_color)
4150 .field("thumb_color", &self.thumb_color)
4151 .field("active_tick_color", &self.active_tick_color)
4152 .field("inactive_tick_color", &self.inactive_tick_color)
4153 .field("disabled_thumb_color", &self.disabled_thumb_color)
4154 .field(
4155 "disabled_active_track_color",
4156 &self.disabled_active_track_color,
4157 )
4158 .field(
4159 "disabled_inactive_track_color",
4160 &self.disabled_inactive_track_color,
4161 )
4162 .field(
4163 "disabled_active_tick_color",
4164 &self.disabled_active_tick_color,
4165 )
4166 .field(
4167 "disabled_inactive_tick_color",
4168 &self.disabled_inactive_tick_color,
4169 )
4170 .field("state_colors", &self.state_colors)
4171 .field(
4172 "on_value_change_finished",
4173 &self.on_value_change_finished.as_ref().map(|_| ".."),
4174 )
4175 .field(
4176 "interaction_source",
4177 &self.interaction_source.as_ref().map(|_| ".."),
4178 )
4179 .finish()
4180 }
4181}
4182
4183impl Default for SliderConfig {
4184 fn default() -> Self {
4185 Self {
4186 modifier: Modifier::new(),
4187 enabled: true,
4188 active_track_color: SliderDefaults::active_track_color(),
4189 inactive_track_color: SliderDefaults::inactive_track_color(),
4190 thumb_color: SliderDefaults::thumb_color(),
4191 active_tick_color: SliderDefaults::active_tick_color(),
4192 inactive_tick_color: SliderDefaults::inactive_tick_color(),
4193 disabled_thumb_color: SliderDefaults::disabled_thumb_color(),
4194 disabled_active_track_color: SliderDefaults::disabled_active_track_color(),
4195 disabled_inactive_track_color: SliderDefaults::disabled_inactive_track_color(),
4196 disabled_active_tick_color: SliderDefaults::disabled_active_tick_color(),
4197 disabled_inactive_tick_color: SliderDefaults::disabled_inactive_tick_color(),
4198 state_colors: SliderDefaults::state_colors_default(),
4199 on_value_change_finished: None,
4200 interaction_source: None,
4201 }
4202 }
4203}
4204
4205static SLIDER_COUNTER: AtomicU64 = AtomicU64::new(0);
4206
4207fn snap_step(v: f32, min: f32, max: f32, step: Option<f32>) -> f32 {
4208 let v = v.clamp(min, max);
4209 if let Some(s) = step.filter(|s| *s > 0.0) {
4210 let t = ((v - min) / s).round();
4211 (min + t * s).clamp(min, max)
4212 } else {
4213 v
4214 }
4215}
4216
4217fn value_from_x(x: f32, rect: Rect, min: f32, max: f32, step: Option<f32>) -> f32 {
4218 let w = rect.w.max(1.0);
4219 let t = ((x - rect.x) / w).clamp(0.0, 1.0);
4220 let v = min + t * (max - min);
4221 snap_step(v, min, max, step)
4222}
4223
4224pub fn Slider(
4225 value: f32,
4226 range: (f32, f32),
4227 step: Option<f32>,
4228 on_change: impl Fn(f32) + 'static,
4229 config: SliderConfig,
4230) -> View {
4231 assert!(range.0 <= range.1, "Slider range start must be <= end");
4232 if let Some(s) = step {
4233 assert!(s > 0.0, "Slider step must be positive");
4234 }
4235 let id = *remember(|| SLIDER_COUNTER.fetch_add(1, Ordering::Relaxed));
4236 let track_rect = remember_state_with_key(format!("ms_rect_{}", id), || Rect::default());
4237 let drag_active = remember_mutable_with_key(format!("ms_da_{}", id), || false);
4238 let hovered = remember(|| Signal::new(false));
4239
4240 let track_rect_p = track_rect.clone();
4241 let drag_active_p = drag_active.clone();
4242 let hovered_sig = hovered.clone();
4243 let sc = config.state_colors;
4244
4245 let min = range.0;
4246 let max = range.1;
4247 let oc = Rc::new(on_change);
4248 let range_size = (max - min).max(1e-6);
4249 let t = ((value - min) / range_size).clamp(0.0, 1.0);
4250
4251 let tick_frac: Vec<f32> = if let Some(s) = step {
4252 let n = ((max - min) / s.max(1e-6)).round() as usize;
4253 (0..=n).map(|i| i as f32 / n as f32).collect()
4254 } else {
4255 Vec::new()
4256 };
4257
4258 let sl_source: Rc<MutableInteractionSource> = config
4259 .interaction_source
4260 .clone()
4261 .map(Rc::new)
4262 .unwrap_or_else(|| remember(MutableInteractionSource::new));
4263 Box(Modifier::new()
4264 .min_width(200.0)
4265 .height(44.0)
4266 .interaction_source(&*sl_source)
4267 .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
4268 let mul_c = |c: Color| {
4269 Color(
4270 c.0,
4271 c.1,
4272 c.2,
4273 ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
4274 )
4275 };
4276 let track_h = dp_to_px(16.0);
4277 let thumb_w = dp_to_px(4.0);
4278 let thumb_h = dp_to_px(44.0);
4279 let dot_r = dp_to_px(2.0);
4280 let corner = track_h * 0.5;
4281 let gap = thumb_w * 0.5 + dp_to_px(ProgressIndicatorDefaults::SLIDER_THUMB_TRACK_GAP);
4282 let pad = thumb_w * 0.5;
4283 let track_x = rect.x + pad;
4284 let track_w = (rect.w - thumb_w).max(0.0);
4285 let cy = rect.y + rect.h * 0.5;
4286
4287 let kx = if step.is_some() && !tick_frac.is_empty() {
4288 let is_first = (t - tick_frac[0]).abs() < 1e-6;
4289 let is_last = (t - tick_frac[tick_frac.len() - 1]).abs() < 1e-6;
4290 if is_first || is_last {
4291 track_x + t * track_w
4292 } else {
4293 track_x + (track_w - track_h) * t + corner
4294 }
4295 } else {
4296 track_x + t * track_w
4297 };
4298
4299 *track_rect_p.borrow_mut() = Rect {
4300 x: track_x,
4301 y: rect.y,
4302 w: track_w,
4303 h: rect.h,
4304 };
4305
4306 let inactive_x = track_x.max(kx + gap);
4307 let inactive_w = (track_x + track_w - inactive_x).max(0.0);
4308 if inactive_w > 0.0 {
4309 scene.nodes.push(SceneNode::Rect {
4310 rect: Rect {
4311 x: inactive_x,
4312 y: cy - track_h * 0.5,
4313 w: inactive_w,
4314 h: track_h,
4315 },
4316 brush: Brush::Solid(mul_c(config.inactive_track_color)),
4317 radius: [corner; 4],
4318 });
4319 let sx = track_x + track_w - corner;
4320 scene.nodes.push(SceneNode::Ellipse {
4321 rect: Rect {
4322 x: sx - dot_r,
4323 y: cy - dot_r,
4324 w: dot_r * 2.0,
4325 h: dot_r * 2.0,
4326 },
4327 brush: Brush::Solid(mul_c(config.inactive_tick_color)),
4328 });
4329 }
4330 let fill_w = (kx - gap - track_x).max(0.0);
4331 if fill_w > 0.0 {
4332 scene.nodes.push(SceneNode::Rect {
4333 rect: Rect {
4334 x: track_x,
4335 y: cy - track_h * 0.5,
4336 w: fill_w,
4337 h: track_h,
4338 },
4339 brush: Brush::Solid(mul_c(config.active_track_color)),
4340 radius: [corner; 4],
4341 });
4342 }
4343 let tick_start = track_x + corner;
4344 let tick_end = track_x + track_w - corner;
4345 for (i, &tf) in tick_frac.iter().enumerate() {
4346 let tx = tick_start + tf * (tick_end - tick_start);
4347 if i == tick_frac.len() - 1 {
4349 continue;
4350 }
4351 if tx >= kx - gap && tx <= kx + gap {
4352 continue;
4353 }
4354 let on_active = tx <= kx - gap;
4355 scene.nodes.push(SceneNode::Ellipse {
4356 rect: Rect {
4357 x: tx - dot_r,
4358 y: cy - dot_r,
4359 w: dot_r * 2.0,
4360 h: dot_r * 2.0,
4361 },
4362 brush: Brush::Solid(mul_c(if on_active {
4363 config.active_tick_color
4364 } else {
4365 config.inactive_tick_color
4366 })),
4367 });
4368 }
4369 let da = *drag_active_p.get();
4370 let hv = hovered_sig.get();
4371 let tw = if da { thumb_w * 0.5 } else { thumb_w };
4372 scene.nodes.push(SceneNode::Rect {
4373 rect: Rect {
4374 x: kx - tw * 0.5,
4375 y: cy - thumb_h * 0.5,
4376 w: tw,
4377 h: thumb_h,
4378 },
4379 brush: Brush::Solid(mul_c(config.thumb_color)),
4380 radius: [tw * 0.5; 4],
4381 });
4382 let sc_target = if da {
4383 sc.pressed
4384 } else if hv {
4385 sc.hovered
4386 } else {
4387 sc.default
4388 };
4389 if sc_target.3 > 0 {
4390 scene.nodes.push(SceneNode::Rect {
4391 rect: Rect {
4392 x: kx - tw * 0.5,
4393 y: cy - thumb_h * 0.5,
4394 w: tw,
4395 h: thumb_h,
4396 },
4397 brush: Brush::Solid(mul_c(sc_target)),
4398 radius: [tw * 0.5; 4],
4399 });
4400 }
4401 })
4402 .on_pointer_enter({
4403 let h = hovered.clone();
4404 move |_pe: PointerEvent| h.set(true)
4405 })
4406 .on_pointer_leave({
4407 let h = hovered.clone();
4408 move |_pe: PointerEvent| h.set(false)
4409 })
4410 .on_pointer_down({
4411 let oc = oc.clone();
4412 let track_rect = track_rect.clone();
4413 let drag_active = drag_active.clone();
4414 move |pe: PointerEvent| {
4415 drag_active.set(true);
4416 let r = *track_rect.borrow();
4417 (oc)(value_from_x(pe.position.x, r, min, max, step));
4418 }
4419 })
4420 .on_pointer_move({
4421 let oc = oc.clone();
4422 let track_rect = track_rect.clone();
4423 let drag_active = drag_active.clone();
4424 move |pe: PointerEvent| {
4425 if !*drag_active.get() {
4426 return;
4427 }
4428 let r = *track_rect.borrow();
4429 (oc)(value_from_x(pe.position.x, r, min, max, step));
4430 }
4431 })
4432 .on_pointer_up({
4433 let on_finished = config.on_value_change_finished.clone();
4434 move |_pe: PointerEvent| {
4435 drag_active.set(false);
4436 if let Some(ref cb) = on_finished {
4437 (cb)();
4438 }
4439 }
4440 })
4441 .on_scroll({
4442 let oc = oc.clone();
4443 move |d: Vec2| -> Vec2 {
4444 let dir = if d.y < -0.5 {
4445 1
4446 } else if d.y > 0.5 {
4447 -1
4448 } else {
4449 0
4450 };
4451 if dir == 0 {
4452 return d;
4453 }
4454 let step_val = step.unwrap_or(1.0).max(1e-6);
4455 let new_val = snap_step(value + (dir as f32) * step_val, min, max, step);
4456 if (new_val - value).abs() > 1e-6 {
4457 (oc)(new_val);
4458 Vec2 { x: d.x, y: 0.0 }
4459 } else {
4460 d
4461 }
4462 }
4463 })
4464 .then(config.modifier))
4465 .semantics(Semantics {
4466 role: Role::Slider,
4467 label: None,
4468 focused: false,
4469 enabled: true,
4470 selectable_group: false,
4471 })
4472}
4473
4474pub fn RangeSlider(
4475 start: f32,
4476 end: f32,
4477 range: (f32, f32),
4478 step: Option<f32>,
4479 on_change: impl Fn(f32, f32) + 'static,
4480 config: SliderConfig,
4481) -> View {
4482 assert!(range.0 <= range.1, "Slider range start must be <= end");
4483 if let Some(s) = step {
4484 assert!(s > 0.0, "Slider step must be positive");
4485 }
4486 let id = *remember(|| SLIDER_COUNTER.fetch_add(1, Ordering::Relaxed));
4487 let track_rect = remember_state_with_key(format!("mrs_rect_{}", id), || Rect::default());
4488 let drag_active = remember_mutable_with_key(format!("mrs_da_{}", id), || false);
4489 let active_thumb = remember_mutable_with_key(format!("mrs_at_{}", id), || false);
4490 let hovered = remember(|| Signal::new(false));
4491
4492 let min = range.0;
4493 let max = range.1;
4494 let oc = Rc::new(on_change);
4495 let range_size = (max - min).max(1e-6);
4496 let t0 = ((start - min) / range_size).clamp(0.0, 1.0);
4497 let t1 = ((end - min) / range_size).clamp(0.0, 1.0);
4498 let sc = config.state_colors;
4499 let is_enabled = config.enabled;
4500
4501 let act_trk = if !is_enabled {
4502 config.disabled_active_track_color
4503 } else {
4504 config.active_track_color
4505 };
4506 let inact_trk = if !is_enabled {
4507 config.disabled_inactive_track_color
4508 } else {
4509 config.inactive_track_color
4510 };
4511 let act_tick = if !is_enabled {
4512 config.disabled_active_tick_color
4513 } else {
4514 config.active_tick_color
4515 };
4516 let inact_tick = if !is_enabled {
4517 config.disabled_inactive_tick_color
4518 } else {
4519 config.inactive_tick_color
4520 };
4521 let thumb_col = if !is_enabled {
4522 config.disabled_thumb_color
4523 } else {
4524 config.thumb_color
4525 };
4526
4527 let tick_frac: Vec<f32> = if let Some(s) = step {
4528 let n = ((max - min) / s.max(1e-6)).round() as usize;
4529 (0..=n).map(|i| i as f32 / n as f32).collect()
4530 } else {
4531 Vec::new()
4532 };
4533
4534 let track_rect_p = track_rect.clone();
4535 let drag_active_p = drag_active.clone();
4536 let active_thumb_p = active_thumb.clone();
4537 let hovered_sig = hovered.clone();
4538
4539 Box(Modifier::new()
4540 .min_width(200.0)
4541 .height(44.0)
4542 .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
4543 let mul_c = |c: Color| {
4544 Color(
4545 c.0,
4546 c.1,
4547 c.2,
4548 ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
4549 )
4550 };
4551 let track_h = dp_to_px(16.0);
4552 let thumb_w = dp_to_px(4.0);
4553 let thumb_h = dp_to_px(44.0);
4554 let dot_r = dp_to_px(2.0);
4555 let corner = track_h * 0.5;
4556 let gap = thumb_w * 0.5 + dp_to_px(ProgressIndicatorDefaults::SLIDER_THUMB_TRACK_GAP);
4557 let pad = thumb_w * 0.5;
4558 let track_x = rect.x + pad;
4559 let track_w = (rect.w - thumb_w).max(0.0);
4560 let cy = rect.y + rect.h * 0.5;
4561
4562 let thumb_pos = |tf: f32, fracs: &[f32]| {
4563 if step.is_some() && !fracs.is_empty() {
4564 let is_first = (tf - fracs[0]).abs() < 1e-6;
4565 let is_last = (tf - fracs[fracs.len() - 1]).abs() < 1e-6;
4566 if is_first || is_last {
4567 track_x + tf * track_w
4568 } else {
4569 track_x + (track_w - track_h) * tf + corner
4570 }
4571 } else {
4572 track_x + tf * track_w
4573 }
4574 };
4575 let k0 = thumb_pos(t0, &tick_frac);
4576 let k1 = thumb_pos(t1, &tick_frac);
4577 let active_l = k0.min(k1);
4578 let active_r = k0.max(k1);
4579
4580 *track_rect_p.borrow_mut() = Rect {
4581 x: track_x,
4582 y: rect.y,
4583 w: track_w,
4584 h: rect.h,
4585 };
4586
4587 let linactive_w = (active_l - gap - track_x).max(0.0);
4588 if linactive_w > 0.0 {
4589 scene.nodes.push(SceneNode::Rect {
4590 rect: Rect {
4591 x: track_x,
4592 y: cy - track_h * 0.5,
4593 w: linactive_w,
4594 h: track_h,
4595 },
4596 brush: Brush::Solid(mul_c(inact_trk)),
4597 radius: [corner; 4],
4598 });
4599 let sx0 = track_x + corner;
4600 scene.nodes.push(SceneNode::Ellipse {
4601 rect: Rect {
4602 x: sx0 - dot_r,
4603 y: cy - dot_r,
4604 w: dot_r * 2.0,
4605 h: dot_r * 2.0,
4606 },
4607 brush: Brush::Solid(mul_c(inact_tick)),
4608 });
4609 }
4610 let rinactive_x = (active_r + gap).min(track_x + track_w);
4611 let rinactive_w = (track_x + track_w - rinactive_x).max(0.0);
4612 if rinactive_w > 0.0 {
4613 scene.nodes.push(SceneNode::Rect {
4614 rect: Rect {
4615 x: rinactive_x,
4616 y: cy - track_h * 0.5,
4617 w: rinactive_w,
4618 h: track_h,
4619 },
4620 brush: Brush::Solid(mul_c(inact_trk)),
4621 radius: [corner; 4],
4622 });
4623 let sx = track_x + track_w - corner;
4624 scene.nodes.push(SceneNode::Ellipse {
4625 rect: Rect {
4626 x: sx - dot_r,
4627 y: cy - dot_r,
4628 w: dot_r * 2.0,
4629 h: dot_r * 2.0,
4630 },
4631 brush: Brush::Solid(mul_c(inact_tick)),
4632 });
4633 }
4634 let active_w = (active_r - gap - (active_l + gap)).max(0.0);
4635 if active_w > 0.0 {
4636 scene.nodes.push(SceneNode::Rect {
4637 rect: Rect {
4638 x: active_l + gap,
4639 y: cy - track_h * 0.5,
4640 w: active_w,
4641 h: track_h,
4642 },
4643 brush: Brush::Solid(mul_c(act_trk)),
4644 radius: [corner; 4],
4645 });
4646 }
4647 let tick_start = track_x + corner;
4648 let tick_end = track_x + track_w - corner;
4649 for (i, &tf) in tick_frac.iter().enumerate() {
4650 let tx = tick_start + tf * (tick_end - tick_start);
4651 if i == 0 || i == tick_frac.len() - 1 {
4653 continue;
4654 }
4655 let in_lgap = tx >= active_l - gap && tx <= active_l + gap;
4656 let in_rgap = tx >= active_r - gap && tx <= active_r + gap;
4657 if in_lgap || in_rgap {
4658 continue;
4659 }
4660 let on_active = tx >= active_l + gap && tx <= active_r - gap;
4661 scene.nodes.push(SceneNode::Ellipse {
4662 rect: Rect {
4663 x: tx - dot_r,
4664 y: cy - dot_r,
4665 w: dot_r * 2.0,
4666 h: dot_r * 2.0,
4667 },
4668 brush: Brush::Solid(mul_c(if on_active { act_tick } else { inact_tick })),
4669 });
4670 }
4671 let da = *drag_active_p.get();
4672 let at = *active_thumb_p.get();
4673 let hv = hovered_sig.get();
4674 let thumbs = [k0, k1];
4675 for (idx, &kx) in thumbs.iter().enumerate() {
4676 let is_active = da && (if idx == 0 { !at } else { at });
4677 let tw = if is_active { thumb_w * 0.5 } else { thumb_w };
4678 scene.nodes.push(SceneNode::Rect {
4679 rect: Rect {
4680 x: kx - tw * 0.5,
4681 y: cy - thumb_h * 0.5,
4682 w: tw,
4683 h: thumb_h,
4684 },
4685 brush: Brush::Solid(mul_c(thumb_col)),
4686 radius: [tw * 0.5; 4],
4687 });
4688 let sc_target = if !is_enabled {
4689 Color::TRANSPARENT
4690 } else if is_active {
4691 sc.pressed
4692 } else if hv {
4693 sc.hovered
4694 } else {
4695 sc.default
4696 };
4697 if sc_target.3 > 0 {
4698 scene.nodes.push(SceneNode::Rect {
4699 rect: Rect {
4700 x: kx - tw * 0.5,
4701 y: cy - thumb_h * 0.5,
4702 w: tw,
4703 h: thumb_h,
4704 },
4705 brush: Brush::Solid(mul_c(sc_target)),
4706 radius: [tw * 0.5; 4],
4707 });
4708 }
4709 }
4710 })
4711 .on_pointer_enter({
4712 let h = hovered.clone();
4713 let en = is_enabled;
4714 move |_pe: PointerEvent| {
4715 if en {
4716 h.set(true);
4717 }
4718 }
4719 })
4720 .on_pointer_leave({
4721 let h = hovered.clone();
4722 move |_pe: PointerEvent| h.set(false)
4723 })
4724 .on_pointer_down({
4725 let oc = oc.clone();
4726 let track_rect = track_rect.clone();
4727 let drag_active = drag_active.clone();
4728 let active_thumb = active_thumb.clone();
4729 let en = is_enabled;
4730 move |pe: PointerEvent| {
4731 if !en {
4732 return;
4733 }
4734 drag_active.set(true);
4735 let r = *track_rect.borrow();
4736 let v = value_from_x(pe.position.x, r, min, max, step);
4737 let use_end = (v - end).abs() < (v - start).abs();
4738 active_thumb.set(use_end);
4739 let (a, b) = if use_end {
4740 (start, v.max(start))
4741 } else {
4742 (v.min(end), end)
4743 };
4744 (oc)(a, b);
4745 }
4746 })
4747 .on_pointer_move({
4748 let oc = oc.clone();
4749 let track_rect = track_rect.clone();
4750 let drag_active = drag_active.clone();
4751 let active_thumb = active_thumb.clone();
4752 move |pe: PointerEvent| {
4753 if !*drag_active.get() {
4754 return;
4755 }
4756 let r = *track_rect.borrow();
4757 let v = value_from_x(pe.position.x, r, min, max, step);
4758 let use_end = *active_thumb.get();
4759 let (a, b) = if use_end {
4760 (start, v.max(start))
4761 } else {
4762 (v.min(end), end)
4763 };
4764 (oc)(a, b);
4765 }
4766 })
4767 .on_pointer_up({
4768 let drag_active = drag_active.clone();
4769 let active_thumb = active_thumb.clone();
4770 move |_pe: PointerEvent| {
4771 drag_active.set(false);
4772 active_thumb.set(false);
4773 }
4774 })
4775 .on_scroll({
4776 let oc = oc.clone();
4777 let active_thumb = active_thumb.clone();
4778 let en = is_enabled;
4779 move |d: Vec2| -> Vec2 {
4780 if !en {
4781 return d;
4782 }
4783 let dir = if d.y < -0.5 {
4784 1
4785 } else if d.y > 0.5 {
4786 -1
4787 } else {
4788 0
4789 };
4790 if dir == 0 {
4791 return d;
4792 }
4793 let step_val = step.unwrap_or(1.0).max(1e-6);
4794 let use_end = *active_thumb.get();
4795 let (mut a, mut b) = (start, end);
4796 if use_end {
4797 b = snap_step(end + (dir as f32) * step_val, min, max, step).max(a);
4798 } else {
4799 a = snap_step(start + (dir as f32) * step_val, min, max, step).min(b);
4800 }
4801 if (a - start).abs() > 1e-6 || (b - end).abs() > 1e-6 {
4802 (oc)(a, b);
4803 Vec2 { x: d.x, y: 0.0 }
4804 } else {
4805 d
4806 }
4807 }
4808 })
4809 .then(config.modifier))
4810 .semantics(Semantics {
4811 role: Role::Slider,
4812 label: None,
4813 focused: false,
4814 enabled: is_enabled,
4815 selectable_group: false,
4816 })
4817}
4818
4819#[derive(Clone, Debug)]
4821pub struct CardConfig {
4822 pub modifier: Modifier,
4823 pub enabled: bool,
4825 pub container_color: Color,
4826 pub content_color: Color,
4827 pub disabled_container_color: Color,
4828 pub disabled_content_color: Color,
4829 pub shape_radius: f32,
4830 pub tonal_elevation: f32,
4831 pub state_elevation: Option<StateElevation>,
4832 pub border: Option<(f32, Color)>,
4833 pub interaction_source: Option<MutableInteractionSource>,
4834}
4835
4836impl Default for CardConfig {
4837 fn default() -> Self {
4838 Self {
4839 modifier: Modifier::new(),
4840 enabled: true,
4841 container_color: CardDefaults::filled_container_color(),
4842 content_color: CardDefaults::filled_content_color(),
4843 disabled_container_color: CardDefaults::disabled_container_color(),
4844 disabled_content_color: CardDefaults::disabled_content_color(),
4845 shape_radius: CardDefaults::SHAPE_RADIUS,
4846 tonal_elevation: CardDefaults::ELEVATION,
4847 state_elevation: None,
4848 border: None,
4849 interaction_source: None,
4850 }
4851 }
4852}
4853
4854pub fn Card(config: CardConfig, content: impl FnOnce() -> View) -> View {
4856 let bg = if !config.enabled {
4857 config.disabled_container_color
4858 } else {
4859 config.container_color
4860 };
4861 let fg = if !config.enabled {
4862 config.disabled_content_color
4863 } else {
4864 config.content_color
4865 };
4866 let source: Rc<MutableInteractionSource> = config
4867 .interaction_source
4868 .clone()
4869 .map(Rc::new)
4870 .unwrap_or_else(|| remember(MutableInteractionSource::new));
4871 let mut m = Modifier::new()
4872 .background(bg)
4873 .clip_rounded(config.shape_radius)
4874 .interaction_source(&*source)
4875 .then(config.modifier);
4876 if let Some((w, c)) = config.border {
4877 m = m.border(w, c, config.shape_radius);
4878 }
4879 if let Some(se) = config.state_elevation {
4880 m = m.state_elevation(se);
4881 } else if config.tonal_elevation > 0.0 {
4882 m = m.state_elevation(StateElevation {
4883 default: config.tonal_elevation,
4884 hovered: config.tonal_elevation,
4885 pressed: config.tonal_elevation,
4886 disabled: 0.0,
4887 });
4888 }
4889 Box(m).color(fg).child(content())
4890}
4891
4892pub fn ElevatedCard(config: CardConfig, content: impl FnOnce() -> View) -> View {
4894 let th = theme();
4895 Card(
4896 CardConfig {
4897 container_color: CardDefaults::elevated_container_color(),
4898 state_elevation: Some(StateElevation {
4899 default: th.elevation.level1,
4900 hovered: th.elevation.level2,
4901 pressed: th.elevation.level3,
4902 disabled: 0.0,
4903 }),
4904 ..config
4905 },
4906 content,
4907 )
4908}
4909
4910pub fn OutlinedCard(config: CardConfig, content: impl FnOnce() -> View) -> View {
4912 Card(
4913 CardConfig {
4914 container_color: CardDefaults::outlined_container_color(),
4915 border: Some((1.0, CardDefaults::outlined_border_color())),
4916 ..config
4917 },
4918 content,
4919 )
4920}
4921
4922fn card_state_colors(bg: Color) -> StateColors {
4923 let th = theme();
4924 StateColors {
4925 default: Color::TRANSPARENT,
4926 hovered: th.on_surface.with_alpha_f32(0.08).composite_over(bg),
4927 pressed: th.on_surface.with_alpha_f32(0.12).composite_over(bg),
4928 disabled: th.on_surface.with_alpha_f32(0.12).composite_over(bg),
4929 }
4930}
4931
4932fn clickable_card_impl(
4933 on_click: impl Fn() + 'static,
4934 modifier: Modifier,
4935 bg: Color,
4936 shape_radius: f32,
4937 config: CardConfig,
4938 content: impl FnOnce() -> View,
4939) -> View {
4940 let m = modifier
4941 .state_colors(card_state_colors(bg))
4942 .clickable()
4943 .on_pointer_down({
4944 let cb = on_click;
4945 let en = config.enabled;
4946 move |_| {
4947 if en {
4948 cb();
4949 }
4950 }
4951 });
4952 Card(
4953 CardConfig {
4954 modifier: m,
4955 enabled: config.enabled,
4956 container_color: bg,
4957 content_color: config.content_color,
4958 disabled_container_color: config.disabled_container_color,
4959 disabled_content_color: config.disabled_content_color,
4960 shape_radius,
4961 border: config.border,
4962 state_elevation: config.state_elevation,
4963 tonal_elevation: config.tonal_elevation,
4964 interaction_source: config.interaction_source.clone(),
4965 },
4966 || Column(Modifier::new().fill_max_size()).child(content()),
4967 )
4968}
4969
4970pub fn ClickableCard(
4972 on_click: impl Fn() + 'static,
4973 modifier: Modifier,
4974 config: CardConfig,
4975 content: impl FnOnce() -> View,
4976) -> View {
4977 let th = theme();
4978 clickable_card_impl(
4979 on_click,
4980 modifier,
4981 th.surface_container_highest,
4982 th.shapes.medium,
4983 config,
4984 content,
4985 )
4986}
4987
4988pub fn ClickableElevatedCard(
4990 on_click: impl Fn() + 'static,
4991 modifier: Modifier,
4992 config: CardConfig,
4993 content: impl FnOnce() -> View,
4994) -> View {
4995 let th = theme();
4996 let cfg = CardConfig {
4997 state_elevation: Some(StateElevation {
4998 default: th.elevation.level1,
4999 hovered: th.elevation.level2,
5000 pressed: th.elevation.level3,
5001 disabled: 0.0,
5002 }),
5003 ..config
5004 };
5005 clickable_card_impl(
5006 on_click,
5007 modifier,
5008 th.surface,
5009 th.shapes.medium,
5010 cfg,
5011 content,
5012 )
5013}
5014
5015pub fn ClickableOutlinedCard(
5017 on_click: impl Fn() + 'static,
5018 modifier: Modifier,
5019 config: CardConfig,
5020 content: impl FnOnce() -> View,
5021) -> View {
5022 let th = theme();
5023 let cfg = CardConfig {
5024 border: Some((1.0, th.outline_variant)),
5025 ..config
5026 };
5027 clickable_card_impl(
5028 on_click,
5029 modifier,
5030 th.surface,
5031 th.shapes.medium,
5032 cfg,
5033 content,
5034 )
5035}
5036
5037#[derive(Clone, Debug)]
5039pub struct SnackbarConfig {
5040 pub modifier: Modifier,
5041 pub container_color: Color,
5042 pub content_color: Color,
5043 pub action_color: Color,
5044 pub dismiss_action_content_color: Color,
5045 pub action_on_new_line: bool,
5046 pub shape_radius: f32,
5047 pub min_height: f32,
5048 pub min_width: f32,
5049 pub max_width: f32,
5050}
5051
5052impl Default for SnackbarConfig {
5053 fn default() -> Self {
5054 Self {
5055 modifier: Modifier::new(),
5056 container_color: SnackbarDefaults::container_color(),
5057 content_color: SnackbarDefaults::content_color(),
5058 action_color: SnackbarDefaults::action_color(),
5059 dismiss_action_content_color: SnackbarDefaults::dismiss_action_content_color(),
5060 action_on_new_line: false,
5061 shape_radius: SnackbarDefaults::SHAPE_RADIUS,
5062 min_height: SnackbarDefaults::MIN_HEIGHT,
5063 min_width: SnackbarDefaults::MIN_WIDTH,
5064 max_width: SnackbarDefaults::MAX_WIDTH,
5065 }
5066 }
5067}
5068
5069#[derive(Clone, Copy, Debug)]
5071pub struct ChipColors {
5072 pub container_color: Color,
5073 pub label_color: Color,
5074 pub leading_icon_color: Color,
5075 pub trailing_icon_color: Color,
5076 pub disabled_container_color: Color,
5077 pub disabled_label_color: Color,
5078 pub disabled_leading_icon_color: Color,
5079 pub disabled_trailing_icon_color: Color,
5080 pub selected_container_color: Color,
5081 pub selected_label_color: Color,
5082 pub selected_leading_icon_color: Color,
5083 pub selected_trailing_icon_color: Color,
5084 pub disabled_selected_container_color: Color,
5085}
5086
5087impl ChipColors {
5088 pub fn container(&self, enabled: bool, selected: bool) -> Color {
5089 match (enabled, selected) {
5090 (true, true) => self.selected_container_color,
5091 (true, false) => self.container_color,
5092 (false, true) => self.disabled_selected_container_color,
5093 (false, false) => self.disabled_container_color,
5094 }
5095 }
5096 pub fn label(&self, enabled: bool, selected: bool) -> Color {
5097 if !enabled {
5098 self.disabled_label_color
5099 } else if selected {
5100 self.selected_label_color
5101 } else {
5102 self.label_color
5103 }
5104 }
5105 pub fn leading_icon(&self, enabled: bool, selected: bool) -> Color {
5106 if !enabled {
5107 self.disabled_leading_icon_color
5108 } else if selected {
5109 self.selected_leading_icon_color
5110 } else {
5111 self.leading_icon_color
5112 }
5113 }
5114 pub fn trailing_icon(&self, enabled: bool, selected: bool) -> Color {
5115 if !enabled {
5116 self.disabled_trailing_icon_color
5117 } else if selected {
5118 self.selected_trailing_icon_color
5119 } else {
5120 self.trailing_icon_color
5121 }
5122 }
5123}
5124
5125#[derive(Clone, Copy, Debug)]
5127pub struct ChipElevation {
5128 pub default: f32,
5129 pub hovered: f32,
5130 pub focused: f32,
5131 pub pressed: f32,
5132 pub dragged: f32,
5133 pub disabled: f32,
5134}
5135
5136impl ChipElevation {
5137 pub fn to_state_elevation(&self) -> StateElevation {
5138 StateElevation {
5139 default: self.default,
5140 hovered: self.hovered,
5141 pressed: self.pressed,
5142 disabled: self.disabled,
5143 }
5144 }
5145}
5146
5147impl Default for ChipElevation {
5148 fn default() -> Self {
5149 Self {
5150 default: ChipDefaults::elevation_default(),
5151 hovered: ChipDefaults::elevation_hovered(),
5152 focused: ChipDefaults::elevation_focused(),
5153 pressed: ChipDefaults::elevation_pressed(),
5154 dragged: ChipDefaults::elevation_dragged(),
5155 disabled: ChipDefaults::elevation_disabled(),
5156 }
5157 }
5158}
5159
5160#[derive(Clone, Debug)]
5162pub struct ChipConfig {
5163 pub modifier: Modifier,
5164 pub enabled: bool,
5165 pub colors: ChipColors,
5166 pub elevation: ChipElevation,
5167 pub border_width: f32,
5168 pub border_color: Color,
5169 pub selected_border_color: Color,
5170 pub disabled_border_color: Color,
5171 pub disabled_selected_border_color: Color,
5172 pub shape_radius: f32,
5173 pub horizontal_padding: f32,
5174 pub interaction_source: Option<MutableInteractionSource>,
5175}
5176
5177impl Default for ChipConfig {
5178 fn default() -> Self {
5179 Self {
5180 modifier: Modifier::new(),
5181 enabled: true,
5182 colors: ChipColors {
5183 container_color: ChipDefaults::container_color(),
5184 label_color: ChipDefaults::label_color(),
5185 leading_icon_color: ChipDefaults::leading_icon_color(),
5186 trailing_icon_color: ChipDefaults::trailing_icon_color(),
5187 disabled_container_color: ChipDefaults::disabled_container_color(),
5188 disabled_label_color: ChipDefaults::disabled_label_color(),
5189 disabled_leading_icon_color: ChipDefaults::disabled_leading_icon_color(),
5190 disabled_trailing_icon_color: ChipDefaults::disabled_trailing_icon_color(),
5191 selected_container_color: ChipDefaults::selected_container_color(),
5192 selected_label_color: ChipDefaults::selected_label_color(),
5193 selected_leading_icon_color: ChipDefaults::selected_leading_icon_color(),
5194 selected_trailing_icon_color: ChipDefaults::selected_trailing_icon_color(),
5195 disabled_selected_container_color: ChipDefaults::disabled_selected_container_color(
5196 ),
5197 },
5198 elevation: ChipElevation::default(),
5199 border_width: ChipDefaults::BORDER_WIDTH,
5200 border_color: ChipDefaults::border_color(),
5201 selected_border_color: ChipDefaults::selected_border_color(),
5202 disabled_border_color: ChipDefaults::disabled_border_color(),
5203 disabled_selected_border_color: ChipDefaults::disabled_selected_border_color(),
5204 shape_radius: ChipDefaults::SHAPE_RADIUS,
5205 horizontal_padding: ChipDefaults::HORIZONTAL_PADDING,
5206 interaction_source: None,
5207 }
5208 }
5209}
5210
5211pub fn AssistChip(
5213 on_click: impl Fn() + 'static,
5214 label: View,
5215 leading_icon: Option<View>,
5216 trailing_icon: Option<View>,
5217 config: ChipConfig,
5218) -> View {
5219 let th = theme();
5220 let is_enabled = config.enabled;
5221 let colors = &config.colors;
5222 let bg = colors.container(is_enabled, false);
5223 let label_color = colors.label(is_enabled, false);
5224 let leading_color = colors.leading_icon(is_enabled, false);
5225 let trailing_color = colors.trailing_icon(is_enabled, false);
5226 let border = if is_enabled {
5227 config.border_color
5228 } else {
5229 config.disabled_border_color
5230 };
5231 let shape = config.shape_radius;
5232 let ch_source: Rc<MutableInteractionSource> = config
5233 .interaction_source
5234 .clone()
5235 .map(Rc::new)
5236 .unwrap_or_else(|| remember(MutableInteractionSource::new));
5237
5238 let mut m = Modifier::new()
5239 .state_colors(StateColors {
5240 default: Color::TRANSPARENT,
5241 hovered: th.on_surface.with_alpha_f32(0.08),
5242 pressed: th.on_surface.with_alpha_f32(0.12),
5243 disabled: Color::TRANSPARENT,
5244 })
5245 .padding_values(PaddingValues {
5246 left: config.horizontal_padding,
5247 right: config.horizontal_padding,
5248 top: 8.0,
5249 bottom: 8.0,
5250 })
5251 .background(bg)
5252 .clip_rounded(shape)
5253 .interaction_source(&*ch_source)
5254 .then(config.modifier);
5255
5256 if config.border_width > 0.0 && border != Color::TRANSPARENT {
5257 m = m.border(config.border_width, border, shape);
5258 }
5259
5260 if is_enabled {
5261 m = m.clickable().on_click(move || on_click());
5262 }
5263
5264 Box(m).child(
5265 Row(Modifier::new().align_items(AlignItems::CENTER)).child((
5266 leading_icon
5267 .map(|v| {
5268 Box(Modifier::new().padding_values(PaddingValues {
5269 left: 0.0,
5270 right: 8.0,
5271 top: 0.0,
5272 bottom: 0.0,
5273 }))
5274 .child(with_content_color(leading_color, move || v))
5275 })
5276 .unwrap_or(Box(Modifier::new())),
5277 with_content_color(label_color, move || label),
5278 trailing_icon
5279 .map(|v| {
5280 Box(Modifier::new().padding_values(PaddingValues {
5281 left: 8.0,
5282 right: 0.0,
5283 top: 0.0,
5284 bottom: 0.0,
5285 }))
5286 .child(with_content_color(trailing_color, move || v))
5287 })
5288 .unwrap_or(Box(Modifier::new())),
5289 )),
5290 )
5291}
5292
5293pub fn ElevatedAssistChip(
5295 on_click: impl Fn() + 'static,
5296 label: View,
5297 leading_icon: Option<View>,
5298 trailing_icon: Option<View>,
5299 config: ChipConfig,
5300) -> View {
5301 let th = theme();
5302 let is_enabled = config.enabled;
5303 let colors = &config.colors;
5304 let bg = colors.container(is_enabled, false);
5305 let label_color = colors.label(is_enabled, false);
5306 let leading_color = colors.leading_icon(is_enabled, false);
5307 let trailing_color = colors.trailing_icon(is_enabled, false);
5308 let shape = config.shape_radius;
5309 let ch_source: Rc<MutableInteractionSource> = config
5310 .interaction_source
5311 .clone()
5312 .map(Rc::new)
5313 .unwrap_or_else(|| remember(MutableInteractionSource::new));
5314
5315 let mut m = Modifier::new()
5316 .state_colors(StateColors {
5317 default: Color::TRANSPARENT,
5318 hovered: th.on_surface.with_alpha_f32(0.08),
5319 pressed: th.on_surface.with_alpha_f32(0.12),
5320 disabled: Color::TRANSPARENT,
5321 })
5322 .state_elevation(config.elevation.to_state_elevation())
5323 .padding_values(PaddingValues {
5324 left: config.horizontal_padding,
5325 right: config.horizontal_padding,
5326 top: 8.0,
5327 bottom: 8.0,
5328 })
5329 .background(bg)
5330 .clip_rounded(shape)
5331 .interaction_source(&*ch_source)
5332 .then(config.modifier);
5333
5334 if is_enabled {
5335 m = m.clickable().on_click(move || on_click());
5336 }
5337
5338 Box(m).child(
5339 Row(Modifier::new().align_items(AlignItems::CENTER)).child((
5340 leading_icon
5341 .map(|v| {
5342 Box(Modifier::new().padding_values(PaddingValues {
5343 left: 0.0,
5344 right: 8.0,
5345 top: 0.0,
5346 bottom: 0.0,
5347 }))
5348 .child(with_content_color(leading_color, move || v))
5349 })
5350 .unwrap_or(Box(Modifier::new())),
5351 with_content_color(label_color, move || label),
5352 trailing_icon
5353 .map(|v| {
5354 Box(Modifier::new().padding_values(PaddingValues {
5355 left: 8.0,
5356 right: 0.0,
5357 top: 0.0,
5358 bottom: 0.0,
5359 }))
5360 .child(with_content_color(trailing_color, move || v))
5361 })
5362 .unwrap_or(Box(Modifier::new())),
5363 )),
5364 )
5365}
5366
5367#[derive(Clone, Debug)]
5369pub struct NavigationBarConfig {
5370 pub modifier: Modifier,
5371 pub container_color: Color,
5372 pub content_color: Color,
5373 pub selected_icon_color: Color,
5374 pub selected_text_color: Color,
5375 pub unselected_icon_color: Color,
5376 pub unselected_text_color: Color,
5377 pub indicator_color: Color,
5378 pub height: f32,
5379 pub tonal_elevation: f32,
5380 pub indicator_opacity: f32,
5381 pub indicator_radius: f32,
5382 pub item_spacing: f32,
5383 pub indicator_width: f32,
5384 pub indicator_height: f32,
5385}
5386
5387impl Default for NavigationBarConfig {
5388 fn default() -> Self {
5389 Self {
5390 modifier: Modifier::new(),
5391 container_color: NavigationBarDefaults::container_color(),
5392 content_color: NavigationBarDefaults::content_color(),
5393 selected_icon_color: NavigationBarDefaults::selected_icon_color(),
5394 selected_text_color: NavigationBarDefaults::selected_text_color(),
5395 unselected_icon_color: NavigationBarDefaults::unselected_icon_color(),
5396 unselected_text_color: NavigationBarDefaults::unselected_text_color(),
5397 indicator_color: NavigationBarDefaults::indicator_color(),
5398 height: NavigationBarDefaults::HEIGHT,
5399 tonal_elevation: NavigationBarDefaults::TONAL_ELEVATION,
5400 indicator_opacity: NavigationBarDefaults::ITEM_ACTIVE_INDICATOR_OPACITY,
5401 indicator_radius: NavigationBarDefaults::INDICATOR_RADIUS,
5402 item_spacing: NavigationBarDefaults::ITEM_SPACING,
5403 indicator_width: NavigationBarDefaults::ACTIVE_INDICATOR_WIDTH,
5404 indicator_height: NavigationBarDefaults::ACTIVE_INDICATOR_HEIGHT,
5405 }
5406 }
5407}
5408
5409#[derive(Clone, Debug)]
5411pub struct NavigationRailConfig {
5412 pub modifier: Modifier,
5413 pub container_color: Color,
5414 pub selected_icon_color: Color,
5415 pub selected_text_color: Color,
5416 pub unselected_icon_color: Color,
5417 pub unselected_text_color: Color,
5418 pub indicator_color: Color,
5419 pub width: f32,
5420 pub item_radius: f32,
5421 pub indicator_opacity: f32,
5422 pub item_spacing: f32,
5423 pub indicator_width: f32,
5424 pub indicator_height: f32,
5425}
5426
5427impl Default for NavigationRailConfig {
5428 fn default() -> Self {
5429 Self {
5430 modifier: Modifier::new(),
5431 container_color: NavigationRailDefaults::container_color(),
5432 selected_icon_color: NavigationRailDefaults::selected_icon_color(),
5433 selected_text_color: NavigationRailDefaults::selected_text_color(),
5434 unselected_icon_color: NavigationRailDefaults::unselected_icon_color(),
5435 unselected_text_color: NavigationRailDefaults::unselected_text_color(),
5436 indicator_color: NavigationRailDefaults::indicator_color(),
5437 width: NavigationRailDefaults::WIDTH,
5438 item_radius: NavigationRailDefaults::ITEM_RADIUS,
5439 indicator_opacity: NavigationRailDefaults::ITEM_ACTIVE_INDICATOR_OPACITY,
5440 item_spacing: NavigationRailDefaults::ITEM_SPACING,
5441 indicator_width: NavigationRailDefaults::ACTIVE_INDICATOR_WIDTH,
5442 indicator_height: NavigationRailDefaults::ACTIVE_INDICATOR_HEIGHT,
5443 }
5444 }
5445}
5446
5447#[derive(Clone, Debug)]
5449pub struct ScaffoldConfig {
5450 pub modifier: Modifier,
5451 pub container_color: Color,
5452 pub top_bar_height: f32,
5453 pub bottom_bar_height: f32,
5454 pub fab_margin: f32,
5455}
5456
5457impl Default for ScaffoldConfig {
5458 fn default() -> Self {
5459 Self {
5460 modifier: Modifier::new(),
5461 container_color: ScaffoldDefaults::container_color(),
5462 top_bar_height: ScaffoldDefaults::TOP_BAR_HEIGHT,
5463 bottom_bar_height: ScaffoldDefaults::BOTTOM_BAR_HEIGHT,
5464 fab_margin: ScaffoldDefaults::FAB_MARGIN,
5465 }
5466 }
5467}
5468
5469#[derive(Clone, Debug)]
5471pub struct NavigationDrawerConfig {
5472 pub modifier: Modifier,
5473 pub container_color: Color,
5474 pub content_color: Color,
5475 pub scrim_color: Color,
5476 pub tonal_elevation: f32,
5477 pub width: f32,
5478 pub shape_radius: f32,
5479}
5480
5481impl Default for NavigationDrawerConfig {
5482 fn default() -> Self {
5483 Self {
5484 modifier: Modifier::new(),
5485 container_color: NavigationDrawerDefaults::container_color(),
5486 content_color: NavigationDrawerDefaults::content_color(),
5487 scrim_color: NavigationDrawerDefaults::scrim_color(),
5488 tonal_elevation: NavigationDrawerDefaults::TONAL_ELEVATION,
5489 width: NavigationDrawerDefaults::WIDTH,
5490 shape_radius: NavigationDrawerDefaults::SHAPE_RADIUS,
5491 }
5492 }
5493}
5494
5495#[derive(Clone, Debug)]
5497pub struct BottomSheetConfig {
5498 pub modifier: Modifier,
5499 pub container_color: Color,
5500 pub content_color: Color,
5501 pub scrim_color: Color,
5502 pub tonal_elevation: f32,
5503 pub shadow_elevation: f32,
5504 pub drag_handle_color: Color,
5505 pub shape_radius: f32,
5506 pub max_width: f32,
5507 pub drag_handle_width: f32,
5508 pub drag_handle_height: f32,
5509 pub peek_height: f32,
5510 pub gestures_enabled: bool,
5511}
5512
5513impl Default for BottomSheetConfig {
5514 fn default() -> Self {
5515 Self {
5516 modifier: Modifier::new(),
5517 container_color: BottomSheetDefaults::container_color(),
5518 content_color: BottomSheetDefaults::content_color(),
5519 scrim_color: BottomSheetDefaults::scrim_color(),
5520 tonal_elevation: BottomSheetDefaults::TONAL_ELEVATION,
5521 shadow_elevation: 0.0,
5522 drag_handle_color: BottomSheetDefaults::drag_handle_color(),
5523 shape_radius: BottomSheetDefaults::SHAPE_RADIUS,
5524 max_width: BottomSheetDefaults::MAX_WIDTH,
5525 drag_handle_width: BottomSheetDefaults::DRAG_HANDLE_WIDTH,
5526 drag_handle_height: BottomSheetDefaults::DRAG_HANDLE_HEIGHT,
5527 peek_height: BottomSheetDefaults::PEEK_HEIGHT,
5528 gestures_enabled: true,
5529 }
5530 }
5531}
5532
5533#[derive(Clone, Copy, Debug)]
5535pub struct SearchBarColors {
5536 pub container_color: Color,
5537 pub active_container_color: Color,
5538 pub divider_color: Color,
5539 pub content_color: Color,
5540 pub placeholder_color: Color,
5541 pub scrim_color: Color,
5542}
5543
5544impl SearchBarColors {
5545 pub fn container(&self, active: bool) -> Color {
5546 if active {
5547 self.active_container_color
5548 } else {
5549 self.container_color
5550 }
5551 }
5552}
5553
5554impl Default for SearchBarColors {
5555 fn default() -> Self {
5556 Self {
5557 container_color: SearchBarDefaults::container_color(),
5558 active_container_color: SearchBarDefaults::active_container_color(),
5559 divider_color: SearchBarDefaults::divider_color(),
5560 content_color: SearchBarDefaults::content_color(),
5561 placeholder_color: SearchBarDefaults::placeholder_color(),
5562 scrim_color: SearchBarDefaults::scrim_color(),
5563 }
5564 }
5565}
5566
5567#[derive(Clone, Copy, Debug)]
5569pub struct AppBarWithSearchColors {
5570 pub search_bar_colors: SearchBarColors,
5571 pub scrolled_search_bar_container_color: Color,
5572 pub app_bar_container_color: Color,
5573 pub scrolled_app_bar_container_color: Color,
5574 pub navigation_icon_content_color: Color,
5575 pub action_icon_content_color: Color,
5576}
5577
5578impl AppBarWithSearchColors {
5579 pub fn search_bar_container(&self, scroll_fraction: f32) -> Color {
5580 lerp_color(
5581 self.search_bar_colors.container_color,
5582 self.scrolled_search_bar_container_color,
5583 scroll_fraction.clamp(0.0, 1.0),
5584 )
5585 }
5586 pub fn app_bar_container(&self, scroll_fraction: f32) -> Color {
5587 lerp_color(
5588 self.app_bar_container_color,
5589 self.scrolled_app_bar_container_color,
5590 scroll_fraction.clamp(0.0, 1.0),
5591 )
5592 }
5593}
5594
5595impl Default for AppBarWithSearchColors {
5596 fn default() -> Self {
5597 Self {
5598 search_bar_colors: SearchBarColors::default(),
5599 scrolled_search_bar_container_color: SearchBarDefaults::scrolled_container_color(),
5600 app_bar_container_color: SearchBarDefaults::app_bar_container_color(),
5601 scrolled_app_bar_container_color: SearchBarDefaults::scrolled_app_bar_container_color(),
5602 navigation_icon_content_color: SearchBarDefaults::navigation_icon_content_color(),
5603 action_icon_content_color: SearchBarDefaults::action_icon_content_color(),
5604 }
5605 }
5606}
5607
5608#[derive(Clone, Debug)]
5610pub struct SearchBarConfig {
5611 pub modifier: Modifier,
5612 pub colors: SearchBarColors,
5613 pub height: f32,
5614 pub shape_radius: f32,
5615 pub active_shape_radius: f32,
5616 pub expanded_width: f32,
5617 pub collapsed_width: f32,
5618 pub tonal_elevation: f32,
5619 pub shadow_elevation: f32,
5620 pub window_insets: WindowInsets,
5621 pub content_padding: PaddingValues,
5622 pub min_width: f32,
5623 pub max_width: f32,
5624}
5625
5626impl Default for SearchBarConfig {
5627 fn default() -> Self {
5628 Self {
5629 modifier: Modifier::new(),
5630 colors: SearchBarColors::default(),
5631 height: SearchBarDefaults::HEIGHT,
5632 shape_radius: SearchBarDefaults::SHAPE_RADIUS,
5633 active_shape_radius: SearchBarDefaults::ACTIVE_SHAPE_RADIUS,
5634 expanded_width: SearchBarDefaults::EXPANDED_WIDTH,
5635 collapsed_width: SearchBarDefaults::COLLAPSED_WIDTH,
5636 tonal_elevation: SearchBarDefaults::TONAL_ELEVATION,
5637 shadow_elevation: SearchBarDefaults::SHADOW_ELEVATION,
5638 window_insets: WindowInsets::default(),
5639 content_padding: SearchBarDefaults::CONTENT_PADDING,
5640 min_width: SearchBarDefaults::MIN_WIDTH,
5641 max_width: SearchBarDefaults::MAX_WIDTH,
5642 }
5643 }
5644}
5645
5646#[derive(Clone, Debug)]
5648pub struct ExpandedFullScreenSearchBarConfig {
5649 pub modifier: Modifier,
5650 pub colors: SearchBarColors,
5651 pub collapsed_shape_radius: f32,
5652 pub tonal_elevation: f32,
5653 pub shadow_elevation: f32,
5654 pub window_insets: WindowInsets,
5655 pub scrim_color: Color,
5656}
5657
5658impl Default for ExpandedFullScreenSearchBarConfig {
5659 fn default() -> Self {
5660 Self {
5661 modifier: Modifier::new(),
5662 colors: SearchBarColors::default(),
5663 collapsed_shape_radius: SearchBarDefaults::SHAPE_RADIUS,
5664 tonal_elevation: SearchBarDefaults::TONAL_ELEVATION,
5665 shadow_elevation: SearchBarDefaults::SHADOW_ELEVATION,
5666 window_insets: WindowInsets::default(),
5667 scrim_color: SearchBarDefaults::scrim_color(),
5668 }
5669 }
5670}
5671
5672#[derive(Clone, Debug)]
5674pub struct ExpandedDockedSearchBarConfig {
5675 pub modifier: Modifier,
5676 pub colors: SearchBarColors,
5677 pub shape_radius: f32,
5678 pub dropdown_shape_radius: f32,
5679 pub dropdown_gap_size: f32,
5680 pub dropdown_scrim_color: Color,
5681 pub tonal_elevation: f32,
5682 pub shadow_elevation: f32,
5683}
5684
5685impl Default for ExpandedDockedSearchBarConfig {
5686 fn default() -> Self {
5687 Self {
5688 modifier: Modifier::new(),
5689 colors: SearchBarColors::default(),
5690 shape_radius: SearchBarDefaults::DOCKED_SHAPE_RADIUS,
5691 dropdown_shape_radius: SearchBarDefaults::DROPDOWN_SHAPE_RADIUS,
5692 dropdown_gap_size: SearchBarDefaults::DROPDOWN_GAP_SIZE,
5693 dropdown_scrim_color: SearchBarDefaults::dropdown_scrim_color(),
5694 tonal_elevation: SearchBarDefaults::TONAL_ELEVATION,
5695 shadow_elevation: SearchBarDefaults::SHADOW_ELEVATION,
5696 }
5697 }
5698}
5699
5700#[derive(Clone, Debug)]
5702pub struct AppBarWithSearchConfig {
5703 pub modifier: Modifier,
5704 pub colors: AppBarWithSearchColors,
5705 pub height: f32,
5706 pub shape_radius: f32,
5707 pub tonal_elevation: f32,
5708 pub shadow_elevation: f32,
5709 pub content_padding: PaddingValues,
5710 pub window_insets: WindowInsets,
5711 pub scroll_fraction: f32,
5712 pub scroll_offset: f32,
5713}
5714
5715impl Default for AppBarWithSearchConfig {
5716 fn default() -> Self {
5717 Self {
5718 modifier: Modifier::new(),
5719 colors: AppBarWithSearchColors::default(),
5720 height: SearchBarDefaults::HEIGHT,
5721 shape_radius: SearchBarDefaults::SHAPE_RADIUS,
5722 tonal_elevation: SearchBarDefaults::TONAL_ELEVATION,
5723 shadow_elevation: SearchBarDefaults::SHADOW_ELEVATION,
5724 content_padding: SearchBarDefaults::CONTENT_PADDING,
5725 window_insets: WindowInsets::default(),
5726 scroll_fraction: 0.0,
5727 scroll_offset: 0.0,
5728 }
5729 }
5730}
5731
5732pub struct SearchBarScrollBehavior {
5734 pub collapsed_offset: Signal<f32>,
5735 pub height: f32,
5736 pub collapsed_height: f32,
5737 _pending: Rc<Cell<f32>>,
5738}
5739
5740impl SearchBarScrollBehavior {
5741 pub fn new(height: f32, collapsed_height: f32) -> Self {
5742 Self {
5743 collapsed_offset: signal(0.0),
5744 height,
5745 collapsed_height,
5746 _pending: Rc::new(Cell::new(0.0)),
5747 }
5748 }
5749
5750 pub fn offset(&self) -> f32 {
5751 self.collapsed_offset.get()
5752 }
5753
5754 pub fn nested_scroll_connection(&self) -> NestedScrollConnection {
5755 let offset = self.collapsed_offset.clone();
5756 let max_offset = self.height - self.collapsed_height;
5757 NestedScrollConnection::new().on_pre_scroll(move |delta: Vec2, _source| {
5758 let cur = offset.get();
5759 let new = (cur - delta.y).clamp(-max_offset, 0.0);
5760 let consumed = cur - new;
5761 offset.set(new);
5762 request_frame();
5763 Vec2 {
5764 x: 0.0,
5765 y: consumed,
5766 }
5767 })
5768 }
5769}
5770
5771#[derive(Clone, Debug)]
5773pub struct DropdownMenuConfig {
5774 pub modifier: Modifier,
5775 pub container_color: Color,
5776 pub item_text_color: Color,
5777 pub disabled_item_text_color: Color,
5778 pub divider_color: Color,
5779 pub min_width: f32,
5780 pub item_height: f32,
5781 pub max_width: f32,
5782 pub shadow_elevation: Option<f32>,
5783 pub tonal_elevation: f32,
5784 pub border: Option<(f32, Color, f32)>,
5785 pub shape_radius: Option<f32>,
5786 pub offset_x: f32,
5787 pub offset_y: f32,
5788 pub vertical_margin: f32,
5789}
5790
5791impl Default for DropdownMenuConfig {
5792 fn default() -> Self {
5793 Self {
5794 modifier: Modifier::new(),
5795 container_color: DropdownMenuDefaults::container_color(),
5796 item_text_color: DropdownMenuDefaults::item_text_color(),
5797 disabled_item_text_color: DropdownMenuDefaults::disabled_item_text_color(),
5798 divider_color: DropdownMenuDefaults::divider_color(),
5799 min_width: DropdownMenuDefaults::MIN_WIDTH,
5800 item_height: DropdownMenuDefaults::ITEM_HEIGHT,
5801 max_width: DropdownMenuDefaults::MAX_WIDTH,
5802 shadow_elevation: None,
5803 tonal_elevation: 0.0,
5804 border: None,
5805 shape_radius: None,
5806 offset_x: 0.0,
5807 offset_y: 0.0,
5808 vertical_margin: DropdownMenuDefaults::VERTICAL_MARGIN,
5809 }
5810 }
5811}
5812
5813#[derive(Clone, Debug)]
5815pub struct TooltipConfig {
5816 pub modifier: Modifier,
5817 pub container_color: Color,
5818 pub content_color: Color,
5819 pub offset_y: f32,
5820 pub horizontal_padding: f32,
5821 pub vertical_padding: f32,
5822 pub has_action: bool,
5823 pub enable_user_input: bool,
5824 pub focusable: bool,
5825 pub max_width: f32,
5826 pub tonal_elevation: f32,
5827 pub shadow_elevation: f32,
5828}
5829
5830impl Default for TooltipConfig {
5831 fn default() -> Self {
5832 Self {
5833 modifier: Modifier::new(),
5834 container_color: TooltipDefaults::container_color(),
5835 content_color: TooltipDefaults::content_color(),
5836 offset_y: TooltipDefaults::OFFSET_Y,
5837 horizontal_padding: TooltipDefaults::HORIZONTAL_PADDING,
5838 vertical_padding: TooltipDefaults::VERTICAL_PADDING,
5839 has_action: false,
5840 enable_user_input: true,
5841 focusable: false,
5842 max_width: TooltipDefaults::MAX_WIDTH,
5843 tonal_elevation: 0.0,
5844 shadow_elevation: 0.0,
5845 }
5846 }
5847}
5848
5849#[derive(Clone, Debug)]
5851pub struct SwipeToDismissConfig {
5852 pub modifier: Modifier,
5853 pub dismiss_threshold: f32,
5854 pub dismissed_offset: f32,
5855 pub animation_spec: AnimationSpec,
5856 pub gestures_enabled: bool,
5857 pub enable_dismiss_from_start_to_end: bool,
5858 pub enable_dismiss_from_end_to_start: bool,
5859}
5860
5861impl Default for SwipeToDismissConfig {
5862 fn default() -> Self {
5863 Self {
5864 modifier: Modifier::new(),
5865 dismiss_threshold: SwipeToDismissDefaults::DISMISS_THRESHOLD,
5866 dismissed_offset: SwipeToDismissDefaults::DISMISSED_OFFSET,
5867 animation_spec: AnimationSpec::spring_gentle(),
5868 gestures_enabled: true,
5869 enable_dismiss_from_start_to_end: true,
5870 enable_dismiss_from_end_to_start: true,
5871 }
5872 }
5873}
5874
5875#[derive(Clone, Debug)]
5877pub struct PullToRefreshConfig {
5878 pub modifier: Modifier,
5879 pub indicator_color: Color,
5880 pub threshold: f32,
5881 pub content_alignment: AlignItems,
5882}
5883
5884impl Default for PullToRefreshConfig {
5885 fn default() -> Self {
5886 Self {
5887 modifier: Modifier::new(),
5888 indicator_color: PullToRefreshDefaults::indicator_color(),
5889 threshold: PullToRefreshDefaults::THRESHOLD,
5890 content_alignment: AlignItems::FLEX_START,
5891 }
5892 }
5893}