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