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