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 Stack(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 = 16.0;
3006 let label_end_y = -min_label_half_h;
3007 let label_y = label_start_y - (label_start_y - label_end_y) * float_t;
3008
3009 let label_start_x = if has_label { 24.0 } else { 0.0 };
3011 let label_end_x = if has_label { 20.0 } else { 0.0 };
3012 let label_x = label_start_x - (label_start_x - label_end_x) * float_t;
3013
3014 let tf_placeholder = if has_label {
3016 if should_float {
3017 config.placeholder.unwrap_or_default()
3018 } else {
3019 String::new()
3020 }
3021 } else {
3022 config.placeholder.unwrap_or_default()
3023 };
3024
3025 let (top_pad, bottom_pad) = if has_label { (8.0, 8.0) } else { (16.0, 16.0) };
3027
3028 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 min_label_half_h: f32 = if has_label { 8.0 } else { 0.0 };
3243
3244 let label_start_y = 16.0;
3245 let label_end_y = -min_label_half_h;
3246 let label_y = label_start_y - (label_start_y - label_end_y) * float_t;
3247
3248 let label_start_x = if has_label { 24.0 } else { 0.0 };
3249 let label_end_x = if has_label { 20.0 } else { 0.0 };
3250 let label_x = label_start_x - (label_start_x - label_end_x) * float_t;
3251
3252 let tf_placeholder = if has_label {
3253 if should_float {
3254 config.placeholder.unwrap_or_default()
3255 } else {
3256 String::new()
3257 }
3258 } else {
3259 config.placeholder.unwrap_or_default()
3260 };
3261
3262 let indicator_active = config.is_error || (config.enabled && is_focused);
3263 let indicator_target_w = if indicator_active { 2.0 } else { 1.0 };
3264 let indicator_w = animate_f32(
3265 format!("tf_ind_w_{}", anim_key),
3266 indicator_target_w,
3267 th.motion.color,
3268 );
3269
3270 let (top_pad, bottom_pad) = if has_label { (8.0, 8.0) } else { (16.0, 16.0) };
3271
3272 Stack(
3273 modifier
3274 .min_height(TextFieldDefaults::MIN_HEIGHT)
3275 .min_width(TextFieldDefaults::MIN_WIDTH),
3276 )
3277 .child((
3278 Box(Modifier::new()
3280 .fill_max_size()
3281 .clip_rounded(th.shapes.extra_small)
3282 .background(container_bg))
3283 .child(
3284 Stack(Modifier::new().fill_max_size()).child((
3285 Row(Modifier::new()
3287 .fill_max_size()
3288 .padding_values(PaddingValues {
3289 left: 16.0,
3290 right: 16.0,
3291 top: top_pad,
3292 bottom: bottom_pad,
3293 })
3294 .align_items(AlignItems::CENTER))
3295 .child((
3296 config.leading_icon.unwrap_or(Box(Modifier::new())),
3297 View::new(0, ViewKind::Box)
3298 .modifier(
3299 Modifier::new().flex_grow(1.0).text_input(TextInputConfig {
3300 hint: tf_placeholder,
3301 multiline: !config.single_line,
3302 on_change: Some(Rc::new(on_value_change) as _),
3303 on_submit: config.on_submit.clone().map(|f| {
3304 let f = f.clone();
3305 Rc::new(move |s| f(s)) as Rc<dyn Fn(String)>
3306 }),
3307 focus_tracker: Some(focus_tracker.clone()),
3308 value: value.clone(),
3309 visual_transformation: None,
3310 keyboard_type: Default::default(),
3311 capitalization: Default::default(),
3312 ime_action: Default::default(),
3313 enabled: config.enabled,
3314 read_only: false,
3315 max_lines: None,
3316 min_lines: 1,
3317 cursor_color: config
3318 .colors
3319 .as_ref()
3320 .map(|c| c.cursor_color(config.is_error)),
3321 on_text_layout: None,
3322 text_style: None,
3323 keyboard_actions: None,
3324 interaction_source: None,
3325 line_limits: None,
3326 }),
3327 )
3328 .semantics(Semantics {
3329 role: Role::TextField,
3330 label: None,
3331 focused: false,
3332 enabled: true,
3333 selectable_group: false,
3334 }),
3335 config.trailing_icon.unwrap_or(Box(Modifier::new())),
3336 )),
3337 Box(Modifier::new()
3339 .fill_max_width()
3340 .height(indicator_w)
3341 .absolute()
3342 .offset(None, None, None, Some(0.0))
3343 .background(indicator_color)),
3344 )),
3345 ),
3346 if let Some(lbl) = label_str {
3348 Box(Modifier::new()
3349 .min_width(200.0)
3350 .padding_values(PaddingValues {
3351 left: label_x,
3352 right: 20.0,
3353 top: 0.0,
3354 bottom: 0.0,
3355 })
3356 .absolute()
3357 .offset(Some(0.0), Some(label_y), None, None))
3358 .child(
3359 Text(lbl.as_ref().to_string())
3360 .color(label_color)
3361 .size(label_size),
3362 )
3363 } else {
3364 Box(Modifier::new())
3365 },
3366 ))
3367}
3368
3369#[derive(Clone, Debug)]
3371pub struct CheckboxConfig {
3372 pub modifier: Modifier,
3373 pub enabled: bool,
3375 pub checked_color: Color,
3376 pub unchecked_color: Color,
3377 pub checkmark_color: Color,
3378 pub checked_border_color: Color,
3380 pub unchecked_border_color: Color,
3382 pub disabled_checked_box_color: Color,
3383 pub disabled_unchecked_box_color: Color,
3384 pub disabled_indeterminate_box_color: Color,
3385 pub disabled_checkmark_color: Color,
3386 pub disabled_checked_border_color: Color,
3387 pub disabled_unchecked_border_color: Color,
3388 pub disabled_indeterminate_border_color: Color,
3389 pub state_colors: StateColors,
3390 pub interaction_source: Option<MutableInteractionSource>,
3391}
3392
3393impl Default for CheckboxConfig {
3394 fn default() -> Self {
3395 Self {
3396 modifier: Modifier::new(),
3397 enabled: true,
3398 checked_color: CheckboxDefaults::checked_color(),
3399 unchecked_color: CheckboxDefaults::unchecked_color(),
3400 checkmark_color: CheckboxDefaults::checkmark_color(),
3401 checked_border_color: CheckboxDefaults::checked_color(),
3402 unchecked_border_color: CheckboxDefaults::unchecked_color(),
3403 disabled_checked_box_color: CheckboxDefaults::disabled_checked_box_color(),
3404 disabled_unchecked_box_color: Color::TRANSPARENT,
3405 disabled_indeterminate_box_color: CheckboxDefaults::disabled_checked_box_color(),
3406 disabled_checkmark_color: CheckboxDefaults::disabled_checkmark_color(),
3407 disabled_checked_border_color: CheckboxDefaults::disabled_checked_box_color(),
3408 disabled_unchecked_border_color: CheckboxDefaults::disabled_unchecked_border_color(),
3409 disabled_indeterminate_border_color: CheckboxDefaults::disabled_checked_box_color(),
3410 state_colors: CheckboxDefaults::state_colors_default(),
3411 interaction_source: None,
3412 }
3413 }
3414}
3415
3416static CHECKBOX_COUNTER: AtomicU64 = AtomicU64::new(0);
3420pub fn Checkbox(checked: bool, on_change: impl Fn(bool) + 'static, config: CheckboxConfig) -> View {
3421 let th = theme();
3422 let sz = CheckboxDefaults::BOX_SIZE;
3423
3424 let id = remember(|| CHECKBOX_COUNTER.fetch_add(1, Ordering::Relaxed));
3425 let spec = th.motion.color_fast;
3426
3427 let is_enabled = config.enabled;
3428
3429 let fill = animate_color(
3430 format!("cb_fill_{}", id),
3431 if !is_enabled {
3432 if checked {
3433 config.disabled_checked_box_color
3434 } else {
3435 config.disabled_unchecked_box_color
3436 }
3437 } else if checked {
3438 config.checked_color
3439 } else {
3440 Color::TRANSPARENT
3441 },
3442 spec,
3443 );
3444 let bd_w = animate_f32(
3445 format!("cb_bw_{}", id),
3446 if !is_enabled && checked {
3447 0.0
3448 } else if !is_enabled {
3449 CheckboxDefaults::STROKE_WIDTH
3450 } else if checked {
3451 0.0
3452 } else {
3453 CheckboxDefaults::STROKE_WIDTH
3454 },
3455 spec,
3456 );
3457 let bd = animate_color(
3458 format!("cb_bd_{}", id),
3459 if !is_enabled {
3460 if checked {
3461 config.disabled_checked_border_color
3462 } else {
3463 config.disabled_unchecked_border_color
3464 }
3465 } else if checked {
3466 Color::TRANSPARENT
3467 } else {
3468 config.unchecked_border_color
3469 },
3470 spec,
3471 );
3472 let check_alpha = animate_f32(
3473 format!("cb_ca_{}", id),
3474 if checked { 1.0 } else { 0.0 },
3475 spec,
3476 );
3477 let check_col = if !is_enabled {
3478 config.disabled_checkmark_color
3479 } else {
3480 config.checkmark_color
3481 };
3482
3483 let cb = move || {
3484 if config.enabled {
3485 on_change(!checked)
3486 }
3487 };
3488
3489 let cb_source: Rc<MutableInteractionSource> = config
3490 .interaction_source
3491 .clone()
3492 .map(Rc::new)
3493 .unwrap_or_else(|| remember(MutableInteractionSource::new));
3494 Box(Modifier::new()
3495 .width(CheckboxDefaults::TOUCH_TARGET_SIZE)
3496 .height(CheckboxDefaults::TOUCH_TARGET_SIZE)
3497 .padding(0.0)
3498 .clip_rounded(20.0)
3499 .background(Color::TRANSPARENT)
3500 .state_colors(config.state_colors)
3501 .interaction_source(&*cb_source)
3502 .clickable()
3503 .align_items(AlignItems::CENTER)
3504 .justify_content(JustifyContent::CENTER)
3505 .on_click(cb)
3506 .then(config.modifier))
3507 .child(
3508 Box(Modifier::new()
3509 .size(sz, sz)
3510 .background(fill)
3511 .border(bd_w, bd, CheckboxDefaults::CORNER_RADIUS)
3512 .clip_rounded(CheckboxDefaults::CORNER_RADIUS)
3513 .align_items(AlignItems::CENTER)
3514 .justify_content(JustifyContent::CENTER))
3515 .child(if check_alpha > 0.01 {
3516 Box(Modifier::new().alpha(check_alpha)).child(
3517 Icon(Symbol::new("done", '\u{E876}'))
3518 .color(check_col)
3519 .size(CheckboxDefaults::CHECK_ICON_SIZE),
3520 )
3521 } else {
3522 Box(Modifier::new())
3523 }),
3524 )
3525}
3526
3527#[derive(Clone, Copy, Debug, PartialEq)]
3529pub enum TriState {
3530 Checked,
3531 Unchecked,
3532 Indeterminate,
3533}
3534
3535pub fn TriStateCheckbox(
3538 state: TriState,
3539 on_change: impl Fn(TriState) + 'static,
3540 config: CheckboxConfig,
3541) -> View {
3542 let th = theme();
3543 let sz = CheckboxDefaults::BOX_SIZE;
3544
3545 let id = remember(|| CHECKBOX_COUNTER.fetch_add(1, Ordering::Relaxed));
3546 let spec = th.motion.color_fast;
3547
3548 let is_checked = state == TriState::Checked;
3549 let is_indeterminate = state == TriState::Indeterminate;
3550 let has_fill = is_checked || is_indeterminate;
3551 let is_enabled = config.enabled;
3552
3553 let fill = animate_color(
3554 format!("tc_fill_{}", id),
3555 if !is_enabled {
3556 if has_fill {
3557 config.disabled_indeterminate_box_color
3558 } else {
3559 config.disabled_unchecked_box_color
3560 }
3561 } else if has_fill {
3562 config.checked_color
3563 } else {
3564 Color::TRANSPARENT
3565 },
3566 spec,
3567 );
3568 let bd_w = animate_f32(
3569 format!("tc_bw_{}", id),
3570 if !is_enabled {
3571 if has_fill {
3572 0.0
3573 } else {
3574 CheckboxDefaults::STROKE_WIDTH
3575 }
3576 } else if has_fill {
3577 0.0
3578 } else {
3579 CheckboxDefaults::STROKE_WIDTH
3580 },
3581 spec,
3582 );
3583 let bd = animate_color(
3584 format!("tc_bd_{}", id),
3585 if !is_enabled {
3586 if has_fill {
3587 config.disabled_indeterminate_border_color
3588 } else {
3589 config.disabled_unchecked_border_color
3590 }
3591 } else if has_fill {
3592 Color::TRANSPARENT
3593 } else {
3594 config.unchecked_border_color
3595 },
3596 spec,
3597 );
3598 let symbol_alpha = animate_f32(
3599 format!("tc_sa_{}", id),
3600 if has_fill { 1.0 } else { 0.0 },
3601 spec,
3602 );
3603 let symbol_col = if !is_enabled {
3604 config.disabled_checkmark_color
3605 } else {
3606 config.checkmark_color
3607 };
3608
3609 Box(Modifier::new()
3610 .width(CheckboxDefaults::TOUCH_TARGET_SIZE)
3611 .height(CheckboxDefaults::TOUCH_TARGET_SIZE)
3612 .padding(0.0)
3613 .clip_rounded(20.0)
3614 .background(Color::TRANSPARENT)
3615 .clickable()
3616 .align_items(AlignItems::CENTER)
3617 .justify_content(JustifyContent::CENTER)
3618 .on_click(move || {
3619 if is_enabled {
3620 on_change(match state {
3621 TriState::Checked => TriState::Unchecked,
3622 TriState::Indeterminate => TriState::Checked,
3623 TriState::Unchecked => TriState::Checked,
3624 })
3625 }
3626 })
3627 .then(config.modifier))
3628 .child(
3629 Box(Modifier::new()
3630 .size(sz, sz)
3631 .background(fill)
3632 .border(bd_w, bd, CheckboxDefaults::CORNER_RADIUS)
3633 .clip_rounded(CheckboxDefaults::CORNER_RADIUS)
3634 .align_items(AlignItems::CENTER)
3635 .justify_content(JustifyContent::CENTER))
3636 .child(if symbol_alpha > 0.01 {
3637 Box(Modifier::new().alpha(symbol_alpha)).child(if is_indeterminate {
3638 Box(Modifier::new()
3640 .width(10.0)
3641 .height(2.0)
3642 .background(symbol_col)
3643 .clip_rounded(1.0))
3644 } else {
3645 Icon(Symbol::new("done", '\u{E876}'))
3646 .color(symbol_col)
3647 .size(CheckboxDefaults::CHECK_ICON_SIZE)
3648 })
3649 } else {
3650 Box(Modifier::new())
3651 }),
3652 )
3653}
3654
3655#[derive(Clone, Debug)]
3657pub struct RadioButtonConfig {
3658 pub modifier: Modifier,
3659 pub enabled: bool,
3661 pub selected_color: Color,
3662 pub unselected_color: Color,
3663 pub disabled_selected_color: Color,
3664 pub disabled_unselected_color: Color,
3665 pub state_colors: StateColors,
3666 pub interaction_source: Option<MutableInteractionSource>,
3667}
3668
3669impl Default for RadioButtonConfig {
3670 fn default() -> Self {
3671 Self {
3672 modifier: Modifier::new(),
3673 enabled: true,
3674 selected_color: RadioButtonDefaults::selected_color(),
3675 unselected_color: RadioButtonDefaults::unselected_color(),
3676 disabled_selected_color: RadioButtonDefaults::disabled_selected_color(),
3677 disabled_unselected_color: RadioButtonDefaults::disabled_unselected_color(),
3678 state_colors: RadioButtonDefaults::state_colors_default(),
3679 interaction_source: None,
3680 }
3681 }
3682}
3683
3684static RADIO_COUNTER: AtomicU64 = AtomicU64::new(0);
3688pub fn RadioButton(
3689 selected: bool,
3690 on_select: impl Fn() + 'static,
3691 config: RadioButtonConfig,
3692) -> View {
3693 let th = theme();
3694 let d = RadioButtonDefaults::OUTER_RADIUS * 2.0;
3695
3696 let id = remember(|| RADIO_COUNTER.fetch_add(1, Ordering::Relaxed));
3697 let color_spec = th.motion.color_fast;
3698 let spring = th.motion.spring;
3699
3700 let ring_col = animate_color(
3701 format!("rb_ring_{}", id),
3702 if !config.enabled {
3703 if selected {
3704 config.disabled_selected_color
3705 } else {
3706 config.disabled_unselected_color
3707 }
3708 } else if selected {
3709 config.selected_color
3710 } else {
3711 config.unselected_color
3712 },
3713 color_spec,
3714 );
3715 let dot_size = animate_f32(
3716 format!("rb_dot_{}", id),
3717 if selected {
3718 RadioButtonDefaults::DOT_RADIUS * 2.0
3719 } else {
3720 0.0
3721 },
3722 spring,
3723 );
3724 let dot_col = if !config.enabled {
3725 config.disabled_selected_color
3726 } else {
3727 config.selected_color
3728 };
3729
3730 let cb = move || {
3731 if config.enabled {
3732 on_select()
3733 }
3734 };
3735
3736 let rb_source: Rc<MutableInteractionSource> = config
3737 .interaction_source
3738 .clone()
3739 .map(Rc::new)
3740 .unwrap_or_else(|| remember(MutableInteractionSource::new));
3741 Box(Modifier::new()
3742 .width(RadioButtonDefaults::TOUCH_TARGET_SIZE)
3743 .height(RadioButtonDefaults::TOUCH_TARGET_SIZE)
3744 .padding(0.0)
3745 .clip_rounded(20.0)
3746 .background(Color::TRANSPARENT)
3747 .state_colors(config.state_colors)
3748 .interaction_source(&*rb_source)
3749 .clickable()
3750 .align_items(AlignItems::CENTER)
3751 .justify_content(JustifyContent::CENTER)
3752 .on_click(cb)
3753 .then(config.modifier))
3754 .child(
3755 Box(Modifier::new()
3756 .size(d, d)
3757 .border(RadioButtonDefaults::STROKE_WIDTH, ring_col, d * 0.5)
3758 .clip_rounded(d * 0.5)
3759 .align_items(AlignItems::CENTER)
3760 .justify_content(JustifyContent::CENTER))
3761 .child(if dot_size > 0.5 {
3762 Box(Modifier::new()
3763 .size(dot_size, dot_size)
3764 .background(dot_col)
3765 .clip_rounded(dot_size * 0.5))
3766 } else {
3767 Box(Modifier::new())
3768 }),
3769 )
3770}
3771
3772#[derive(Clone, Debug)]
3774pub struct SwitchConfig {
3775 pub modifier: Modifier,
3776 pub enabled: bool,
3778 pub checked_track_color: Color,
3779 pub unchecked_track_color: Color,
3780 pub checked_thumb_color: Color,
3781 pub unchecked_thumb_color: Color,
3782 pub checked_icon_color: Color,
3784 pub unchecked_icon_color: Color,
3786 pub checked_border_color: Color,
3788 pub unchecked_border_color: Color,
3790 pub disabled_checked_thumb_color: Color,
3791 pub disabled_checked_track_color: Color,
3792 pub disabled_checked_border_color: Color,
3793 pub disabled_checked_icon_color: Color,
3794 pub disabled_unchecked_thumb_color: Color,
3795 pub disabled_unchecked_track_color: Color,
3796 pub disabled_unchecked_border_color: Color,
3797 pub disabled_unchecked_icon_color: Color,
3798 pub state_colors: StateColors,
3799 pub thumb_content: Option<View>,
3800 pub interaction_source: Option<MutableInteractionSource>,
3801}
3802
3803impl Default for SwitchConfig {
3804 fn default() -> Self {
3805 Self {
3806 modifier: Modifier::new(),
3807 enabled: true,
3808 checked_track_color: SwitchDefaults::checked_track_color(),
3809 unchecked_track_color: SwitchDefaults::unchecked_track_color(),
3810 checked_thumb_color: SwitchDefaults::checked_thumb_color(),
3811 unchecked_thumb_color: SwitchDefaults::unchecked_thumb_color(),
3812 checked_icon_color: SwitchDefaults::checked_icon_color(),
3813 unchecked_icon_color: SwitchDefaults::unchecked_icon_color(),
3814 checked_border_color: Color::TRANSPARENT,
3815 unchecked_border_color: SwitchDefaults::unchecked_border_color(),
3816 disabled_checked_thumb_color: SwitchDefaults::disabled_checked_thumb_color(),
3817 disabled_checked_track_color: SwitchDefaults::disabled_checked_track_color(),
3818 disabled_checked_border_color: Color::TRANSPARENT,
3819 disabled_checked_icon_color: SwitchDefaults::disabled_checked_icon_color(),
3820 disabled_unchecked_thumb_color: SwitchDefaults::disabled_unchecked_thumb_color(),
3821 disabled_unchecked_track_color: SwitchDefaults::disabled_unchecked_track_color(),
3822 disabled_unchecked_border_color: SwitchDefaults::disabled_unchecked_border_color(),
3823 disabled_unchecked_icon_color: SwitchDefaults::disabled_unchecked_icon_color(),
3824 state_colors: SwitchDefaults::state_colors_default(),
3825 thumb_content: None,
3826 interaction_source: None,
3827 }
3828 }
3829}
3830
3831static SWITCH_COUNTER: AtomicU64 = AtomicU64::new(0);
3835pub fn Switch(checked: bool, on_change: impl Fn(bool) + 'static, config: SwitchConfig) -> View {
3836 let th = theme();
3837 let track_w = SwitchDefaults::TRACK_WIDTH;
3838 let track_h = SwitchDefaults::TRACK_HEIGHT;
3839
3840 let id = remember(|| SWITCH_COUNTER.fetch_add(1, Ordering::Relaxed));
3841
3842 let hovered = remember(|| Signal::new(false));
3843 let pressed = remember(|| Signal::new(false));
3844
3845 let thumb_target_pos = if checked {
3847 track_w - SwitchDefaults::THUMB_CHECKED_SIZE - 4.0
3848 } else {
3849 8.0
3850 };
3851 let thumb_target_d = if checked {
3852 SwitchDefaults::THUMB_CHECKED_SIZE
3853 } else {
3854 SwitchDefaults::THUMB_UNCHECKED_SIZE
3855 };
3856 let spring = th.motion.spring;
3857
3858 let thumb_left = animate_f32(format!("sw_pos_{}", id), thumb_target_pos, spring);
3859 let thumb_d = animate_f32(format!("sw_d_{}", id), thumb_target_d, spring);
3860 let thumb_top = (track_h - thumb_d) * 0.5;
3861
3862 let color_spec = th.motion.color_fast;
3863 let is_enabled = config.enabled;
3864
3865 let track_bg = animate_color(
3866 format!("sw_tbg_{}", id),
3867 if !is_enabled {
3868 if checked {
3869 config.disabled_checked_track_color
3870 } else {
3871 config.disabled_unchecked_track_color
3872 }
3873 } else if checked {
3874 config.checked_track_color
3875 } else {
3876 config.unchecked_track_color
3877 },
3878 color_spec,
3879 );
3880 let thumb_bg = animate_color(
3881 format!("sw_tmbg_{}", id),
3882 if !is_enabled {
3883 if checked {
3884 config.disabled_checked_thumb_color
3885 } else {
3886 config.disabled_unchecked_thumb_color
3887 }
3888 } else if checked {
3889 config.checked_thumb_color
3890 } else {
3891 config.unchecked_thumb_color
3892 },
3893 color_spec,
3894 );
3895 let track_border = animate_f32(
3896 format!("sw_tb_{}", id),
3897 if !is_enabled {
3898 if checked { 0.0 } else { 2.0 }
3899 } else if checked {
3900 0.0
3901 } else {
3902 2.0
3903 },
3904 color_spec,
3905 );
3906 let border_color = animate_color(
3907 format!("sw_bc_{}", id),
3908 if !is_enabled {
3909 if checked {
3910 config.disabled_checked_border_color
3911 } else {
3912 config.disabled_unchecked_border_color
3913 }
3914 } else if checked {
3915 config.checked_border_color
3916 } else {
3917 config.unchecked_border_color
3918 },
3919 color_spec,
3920 );
3921
3922 let state_overlay = animate_color(
3923 format!("sw_ol_{}", id),
3924 if !is_enabled {
3925 Color::TRANSPARENT
3926 } else if pressed.get() {
3927 config.state_colors.pressed
3928 } else if hovered.get() {
3929 config.state_colors.hovered
3930 } else {
3931 config.state_colors.default
3932 },
3933 color_spec,
3934 );
3935
3936 let sw_source: Rc<MutableInteractionSource> = config
3937 .interaction_source
3938 .clone()
3939 .map(Rc::new)
3940 .unwrap_or_else(|| remember(MutableInteractionSource::new));
3941 Box(Modifier::new()
3942 .size(track_w, track_h)
3943 .padding(0.0)
3944 .clip_rounded(track_h * 0.5)
3945 .background(track_bg)
3946 .border(track_border, border_color, track_h * 0.5)
3947 .interaction_source(&*sw_source)
3948 .clickable()
3949 .on_pointer_enter({
3950 let h = hovered.clone();
3951 move |_| h.set(true)
3952 })
3953 .on_pointer_leave({
3954 let h = hovered.clone();
3955 let p = pressed.clone();
3956 move |_| {
3957 h.set(false);
3958 p.set(false);
3959 }
3960 })
3961 .on_pointer_down({
3962 let p = pressed.clone();
3963 move |_| p.set(true)
3964 })
3965 .on_click({
3966 let cb = on_change;
3967 move || cb(!checked)
3968 })
3969 .on_pointer_up({
3970 let p = pressed.clone();
3971 move |_| p.set(false)
3972 })
3973 .then(config.modifier))
3974 .child((
3975 Box(Modifier::new()
3976 .size(thumb_d, thumb_d)
3977 .background(thumb_bg)
3978 .clip_rounded(thumb_d * 0.5)
3979 .hit_passthrough()
3980 .absolute()
3981 .offset(Some(thumb_left), Some(thumb_top), None, None)),
3982 Box(Modifier::new()
3983 .size(40.0, 40.0)
3984 .clip_rounded(20.0)
3985 .background(state_overlay)
3986 .hit_passthrough()
3987 .absolute()
3988 .offset(
3989 Some(thumb_left + thumb_d * 0.5 - 20.0),
3990 Some(track_h * 0.5 - 20.0),
3991 None,
3992 None,
3993 )),
3994 ))
3995}
3996
3997#[derive(Clone)]
3999pub struct SliderConfig {
4000 pub modifier: Modifier,
4002 pub enabled: bool,
4004 pub active_track_color: Color,
4005 pub inactive_track_color: Color,
4006 pub thumb_color: Color,
4007 pub active_tick_color: Color,
4008 pub inactive_tick_color: Color,
4009 pub disabled_thumb_color: Color,
4010 pub disabled_active_track_color: Color,
4011 pub disabled_inactive_track_color: Color,
4012 pub disabled_active_tick_color: Color,
4013 pub disabled_inactive_tick_color: Color,
4014 pub state_colors: StateColors,
4015 pub on_value_change_finished: Option<Rc<dyn Fn()>>,
4016 pub interaction_source: Option<MutableInteractionSource>,
4017}
4018
4019impl std::fmt::Debug for SliderConfig {
4020 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4021 f.debug_struct("SliderConfig")
4022 .field("modifier", &self.modifier)
4023 .field("enabled", &self.enabled)
4024 .field("active_track_color", &self.active_track_color)
4025 .field("inactive_track_color", &self.inactive_track_color)
4026 .field("thumb_color", &self.thumb_color)
4027 .field("active_tick_color", &self.active_tick_color)
4028 .field("inactive_tick_color", &self.inactive_tick_color)
4029 .field("disabled_thumb_color", &self.disabled_thumb_color)
4030 .field(
4031 "disabled_active_track_color",
4032 &self.disabled_active_track_color,
4033 )
4034 .field(
4035 "disabled_inactive_track_color",
4036 &self.disabled_inactive_track_color,
4037 )
4038 .field(
4039 "disabled_active_tick_color",
4040 &self.disabled_active_tick_color,
4041 )
4042 .field(
4043 "disabled_inactive_tick_color",
4044 &self.disabled_inactive_tick_color,
4045 )
4046 .field("state_colors", &self.state_colors)
4047 .field(
4048 "on_value_change_finished",
4049 &self.on_value_change_finished.as_ref().map(|_| ".."),
4050 )
4051 .field(
4052 "interaction_source",
4053 &self.interaction_source.as_ref().map(|_| ".."),
4054 )
4055 .finish()
4056 }
4057}
4058
4059impl Default for SliderConfig {
4060 fn default() -> Self {
4061 Self {
4062 modifier: Modifier::new(),
4063 enabled: true,
4064 active_track_color: SliderDefaults::active_track_color(),
4065 inactive_track_color: SliderDefaults::inactive_track_color(),
4066 thumb_color: SliderDefaults::thumb_color(),
4067 active_tick_color: SliderDefaults::active_tick_color(),
4068 inactive_tick_color: SliderDefaults::inactive_tick_color(),
4069 disabled_thumb_color: SliderDefaults::disabled_thumb_color(),
4070 disabled_active_track_color: SliderDefaults::disabled_active_track_color(),
4071 disabled_inactive_track_color: SliderDefaults::disabled_inactive_track_color(),
4072 disabled_active_tick_color: SliderDefaults::disabled_active_tick_color(),
4073 disabled_inactive_tick_color: SliderDefaults::disabled_inactive_tick_color(),
4074 state_colors: SliderDefaults::state_colors_default(),
4075 on_value_change_finished: None,
4076 interaction_source: None,
4077 }
4078 }
4079}
4080
4081static SLIDER_COUNTER: AtomicU64 = AtomicU64::new(0);
4082
4083fn snap_step(v: f32, min: f32, max: f32, step: Option<f32>) -> f32 {
4084 let v = v.clamp(min, max);
4085 if let Some(s) = step.filter(|s| *s > 0.0) {
4086 let t = ((v - min) / s).round();
4087 (min + t * s).clamp(min, max)
4088 } else {
4089 v
4090 }
4091}
4092
4093fn value_from_x(x: f32, rect: Rect, min: f32, max: f32, step: Option<f32>) -> f32 {
4094 let w = rect.w.max(1.0);
4095 let t = ((x - rect.x) / w).clamp(0.0, 1.0);
4096 let v = min + t * (max - min);
4097 snap_step(v, min, max, step)
4098}
4099
4100pub fn Slider(
4101 value: f32,
4102 range: (f32, f32),
4103 step: Option<f32>,
4104 on_change: impl Fn(f32) + 'static,
4105 config: SliderConfig,
4106) -> View {
4107 assert!(range.0 <= range.1, "Slider range start must be <= end");
4108 if let Some(s) = step {
4109 assert!(s > 0.0, "Slider step must be positive");
4110 }
4111 let id = *remember(|| SLIDER_COUNTER.fetch_add(1, Ordering::Relaxed));
4112 let track_rect = remember_state_with_key(format!("ms_rect_{}", id), || Rect::default());
4113 let drag_active = remember_state_with_key(format!("ms_da_{}", id), || false);
4114 let hovered = remember(|| Signal::new(false));
4115
4116 let track_rect_p = track_rect.clone();
4117 let drag_active_p = drag_active.clone();
4118 let hovered_sig = hovered.clone();
4119 let sc = config.state_colors;
4120
4121 let min = range.0;
4122 let max = range.1;
4123 let oc = Rc::new(on_change);
4124 let range_size = (max - min).max(1e-6);
4125 let t = ((value - min) / range_size).clamp(0.0, 1.0);
4126
4127 let tick_frac: Vec<f32> = if let Some(s) = step {
4128 let n = ((max - min) / s.max(1e-6)).round() as usize;
4129 (0..=n).map(|i| i as f32 / n as f32).collect()
4130 } else {
4131 Vec::new()
4132 };
4133
4134 let sl_source: Rc<MutableInteractionSource> = config
4135 .interaction_source
4136 .clone()
4137 .map(Rc::new)
4138 .unwrap_or_else(|| remember(MutableInteractionSource::new));
4139 Box(Modifier::new()
4140 .min_width(200.0)
4141 .height(44.0)
4142 .interaction_source(&*sl_source)
4143 .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
4144 let mul_c = |c: Color| {
4145 Color(
4146 c.0,
4147 c.1,
4148 c.2,
4149 ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
4150 )
4151 };
4152 let track_h = dp_to_px(16.0);
4153 let thumb_w = dp_to_px(4.0);
4154 let thumb_h = dp_to_px(44.0);
4155 let dot_r = dp_to_px(2.0);
4156 let corner = track_h * 0.5;
4157 let gap = thumb_w * 0.5 + dp_to_px(ProgressIndicatorDefaults::SLIDER_THUMB_TRACK_GAP);
4158 let pad = thumb_w * 0.5;
4159 let track_x = rect.x + pad;
4160 let track_w = (rect.w - thumb_w).max(0.0);
4161 let cy = rect.y + rect.h * 0.5;
4162
4163 let kx = if step.is_some() && !tick_frac.is_empty() {
4164 let is_first = (t - tick_frac[0]).abs() < 1e-6;
4165 let is_last = (t - tick_frac[tick_frac.len() - 1]).abs() < 1e-6;
4166 if is_first || is_last {
4167 track_x + t * track_w
4168 } else {
4169 track_x + (track_w - track_h) * t + corner
4170 }
4171 } else {
4172 track_x + t * track_w
4173 };
4174
4175 *track_rect_p.borrow_mut() = Rect {
4176 x: track_x,
4177 y: rect.y,
4178 w: track_w,
4179 h: rect.h,
4180 };
4181
4182 let inactive_x = track_x.max(kx + gap);
4183 let inactive_w = (track_x + track_w - inactive_x).max(0.0);
4184 if inactive_w > 0.0 {
4185 scene.nodes.push(SceneNode::Rect {
4186 rect: Rect {
4187 x: inactive_x,
4188 y: cy - track_h * 0.5,
4189 w: inactive_w,
4190 h: track_h,
4191 },
4192 brush: Brush::Solid(mul_c(config.inactive_track_color)),
4193 radius: [corner; 4],
4194 });
4195 }
4196 let fill_w = (kx - gap - track_x).max(0.0);
4197 if fill_w > 0.0 {
4198 scene.nodes.push(SceneNode::Rect {
4199 rect: Rect {
4200 x: track_x,
4201 y: cy - track_h * 0.5,
4202 w: fill_w,
4203 h: track_h,
4204 },
4205 brush: Brush::Solid(mul_c(config.active_track_color)),
4206 radius: [corner; 4],
4207 });
4208 }
4209 let tick_start = track_x + corner;
4210 let tick_end = track_x + track_w - corner;
4211 for (i, &tf) in tick_frac.iter().enumerate() {
4212 let tx = tick_start + tf * (tick_end - tick_start);
4213 if i == tick_frac.len() - 1 {
4215 continue;
4216 }
4217 if tx >= kx - gap && tx <= kx + gap {
4218 continue;
4219 }
4220 let on_active = tx <= kx - gap;
4221 scene.nodes.push(SceneNode::Ellipse {
4222 rect: Rect {
4223 x: tx - dot_r,
4224 y: cy - dot_r,
4225 w: dot_r * 2.0,
4226 h: dot_r * 2.0,
4227 },
4228 brush: Brush::Solid(mul_c(if on_active {
4229 config.active_tick_color
4230 } else {
4231 config.inactive_tick_color
4232 })),
4233 });
4234 }
4235 let sx = track_x + track_w - corner;
4236 scene.nodes.push(SceneNode::Ellipse {
4237 rect: Rect {
4238 x: sx - dot_r,
4239 y: cy - dot_r,
4240 w: dot_r * 2.0,
4241 h: dot_r * 2.0,
4242 },
4243 brush: Brush::Solid(mul_c(config.inactive_tick_color)),
4244 });
4245 let da = *drag_active_p.borrow();
4246 let hv = hovered_sig.get();
4247 let tw = if da { thumb_w * 0.5 } else { thumb_w };
4248 scene.nodes.push(SceneNode::Rect {
4249 rect: Rect {
4250 x: kx - tw * 0.5,
4251 y: cy - thumb_h * 0.5,
4252 w: tw,
4253 h: thumb_h,
4254 },
4255 brush: Brush::Solid(mul_c(config.thumb_color)),
4256 radius: [tw * 0.5; 4],
4257 });
4258 let sc_target = if da {
4259 sc.pressed
4260 } else if hv {
4261 sc.hovered
4262 } else {
4263 sc.default
4264 };
4265 if sc_target.3 > 0 {
4266 scene.nodes.push(SceneNode::Rect {
4267 rect: Rect {
4268 x: kx - tw * 0.5,
4269 y: cy - thumb_h * 0.5,
4270 w: tw,
4271 h: thumb_h,
4272 },
4273 brush: Brush::Solid(mul_c(sc_target)),
4274 radius: [tw * 0.5; 4],
4275 });
4276 }
4277 })
4278 .on_pointer_enter({
4279 let h = hovered.clone();
4280 move |_pe: PointerEvent| h.set(true)
4281 })
4282 .on_pointer_leave({
4283 let h = hovered.clone();
4284 move |_pe: PointerEvent| h.set(false)
4285 })
4286 .on_pointer_down({
4287 let oc = oc.clone();
4288 let track_rect = track_rect.clone();
4289 let drag_active = drag_active.clone();
4290 move |pe: PointerEvent| {
4291 *drag_active.borrow_mut() = true;
4292 let r = *track_rect.borrow();
4293 (oc)(value_from_x(pe.position.x, r, min, max, step));
4294 }
4295 })
4296 .on_pointer_move({
4297 let oc = oc.clone();
4298 let track_rect = track_rect.clone();
4299 let drag_active = drag_active.clone();
4300 move |pe: PointerEvent| {
4301 if !*drag_active.borrow() {
4302 return;
4303 }
4304 let r = *track_rect.borrow();
4305 (oc)(value_from_x(pe.position.x, r, min, max, step));
4306 }
4307 })
4308 .on_pointer_up({
4309 let on_finished = config.on_value_change_finished.clone();
4310 move |_pe: PointerEvent| {
4311 *drag_active.borrow_mut() = false;
4312 if let Some(ref cb) = on_finished {
4313 (cb)();
4314 }
4315 }
4316 })
4317 .on_scroll({
4318 let oc = oc.clone();
4319 move |d: Vec2| -> Vec2 {
4320 let dir = if d.y < -0.5 {
4321 1
4322 } else if d.y > 0.5 {
4323 -1
4324 } else {
4325 0
4326 };
4327 if dir == 0 {
4328 return d;
4329 }
4330 let step_val = step.unwrap_or(1.0).max(1e-6);
4331 let new_val = snap_step(value + (dir as f32) * step_val, min, max, step);
4332 if (new_val - value).abs() > 1e-6 {
4333 (oc)(new_val);
4334 Vec2 { x: d.x, y: 0.0 }
4335 } else {
4336 d
4337 }
4338 }
4339 })
4340 .then(config.modifier))
4341 .semantics(Semantics {
4342 role: Role::Slider,
4343 label: None,
4344 focused: false,
4345 enabled: true,
4346 selectable_group: false,
4347 })
4348}
4349
4350pub fn RangeSlider(
4351 start: f32,
4352 end: f32,
4353 range: (f32, f32),
4354 step: Option<f32>,
4355 on_change: impl Fn(f32, f32) + 'static,
4356 config: SliderConfig,
4357) -> View {
4358 assert!(range.0 <= range.1, "Slider range start must be <= end");
4359 if let Some(s) = step {
4360 assert!(s > 0.0, "Slider step must be positive");
4361 }
4362 let id = *remember(|| SLIDER_COUNTER.fetch_add(1, Ordering::Relaxed));
4363 let track_rect = remember_state_with_key(format!("mrs_rect_{}", id), || Rect::default());
4364 let drag_active = remember_state_with_key(format!("mrs_da_{}", id), || false);
4365 let active_thumb = remember_state_with_key(format!("mrs_at_{}", id), || false);
4366 let hovered = remember(|| Signal::new(false));
4367
4368 let min = range.0;
4369 let max = range.1;
4370 let oc = Rc::new(on_change);
4371 let range_size = (max - min).max(1e-6);
4372 let t0 = ((start - min) / range_size).clamp(0.0, 1.0);
4373 let t1 = ((end - min) / range_size).clamp(0.0, 1.0);
4374 let sc = config.state_colors;
4375 let is_enabled = config.enabled;
4376
4377 let act_trk = if !is_enabled {
4378 config.disabled_active_track_color
4379 } else {
4380 config.active_track_color
4381 };
4382 let inact_trk = if !is_enabled {
4383 config.disabled_inactive_track_color
4384 } else {
4385 config.inactive_track_color
4386 };
4387 let act_tick = if !is_enabled {
4388 config.disabled_active_tick_color
4389 } else {
4390 config.active_tick_color
4391 };
4392 let inact_tick = if !is_enabled {
4393 config.disabled_inactive_tick_color
4394 } else {
4395 config.inactive_tick_color
4396 };
4397 let thumb_col = if !is_enabled {
4398 config.disabled_thumb_color
4399 } else {
4400 config.thumb_color
4401 };
4402
4403 let tick_frac: Vec<f32> = if let Some(s) = step {
4404 let n = ((max - min) / s.max(1e-6)).round() as usize;
4405 (0..=n).map(|i| i as f32 / n as f32).collect()
4406 } else {
4407 Vec::new()
4408 };
4409
4410 let track_rect_p = track_rect.clone();
4411 let drag_active_p = drag_active.clone();
4412 let active_thumb_p = active_thumb.clone();
4413 let hovered_sig = hovered.clone();
4414
4415 Box(Modifier::new()
4416 .min_width(200.0)
4417 .height(44.0)
4418 .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
4419 let mul_c = |c: Color| {
4420 Color(
4421 c.0,
4422 c.1,
4423 c.2,
4424 ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
4425 )
4426 };
4427 let track_h = dp_to_px(16.0);
4428 let thumb_w = dp_to_px(4.0);
4429 let thumb_h = dp_to_px(44.0);
4430 let dot_r = dp_to_px(2.0);
4431 let corner = track_h * 0.5;
4432 let gap = thumb_w * 0.5 + dp_to_px(ProgressIndicatorDefaults::SLIDER_THUMB_TRACK_GAP);
4433 let pad = thumb_w * 0.5;
4434 let track_x = rect.x + pad;
4435 let track_w = (rect.w - thumb_w).max(0.0);
4436 let cy = rect.y + rect.h * 0.5;
4437
4438 let thumb_pos = |tf: f32, fracs: &[f32]| {
4439 if step.is_some() && !fracs.is_empty() {
4440 let is_first = (tf - fracs[0]).abs() < 1e-6;
4441 let is_last = (tf - fracs[fracs.len() - 1]).abs() < 1e-6;
4442 if is_first || is_last {
4443 track_x + tf * track_w
4444 } else {
4445 track_x + (track_w - track_h) * tf + corner
4446 }
4447 } else {
4448 track_x + tf * track_w
4449 }
4450 };
4451 let k0 = thumb_pos(t0, &tick_frac);
4452 let k1 = thumb_pos(t1, &tick_frac);
4453 let active_l = k0.min(k1);
4454 let active_r = k0.max(k1);
4455
4456 *track_rect_p.borrow_mut() = Rect {
4457 x: track_x,
4458 y: rect.y,
4459 w: track_w,
4460 h: rect.h,
4461 };
4462
4463 let linactive_w = (active_l - gap - track_x).max(0.0);
4464 if linactive_w > 0.0 {
4465 scene.nodes.push(SceneNode::Rect {
4466 rect: Rect {
4467 x: track_x,
4468 y: cy - track_h * 0.5,
4469 w: linactive_w,
4470 h: track_h,
4471 },
4472 brush: Brush::Solid(mul_c(inact_trk)),
4473 radius: [corner; 4],
4474 });
4475 }
4476 let rinactive_x = (active_r + gap).min(track_x + track_w);
4477 let rinactive_w = (track_x + track_w - rinactive_x).max(0.0);
4478 if rinactive_w > 0.0 {
4479 scene.nodes.push(SceneNode::Rect {
4480 rect: Rect {
4481 x: rinactive_x,
4482 y: cy - track_h * 0.5,
4483 w: rinactive_w,
4484 h: track_h,
4485 },
4486 brush: Brush::Solid(mul_c(inact_trk)),
4487 radius: [corner; 4],
4488 });
4489 }
4490 let active_w = (active_r - gap - (active_l + gap)).max(0.0);
4491 if active_w > 0.0 {
4492 scene.nodes.push(SceneNode::Rect {
4493 rect: Rect {
4494 x: active_l + gap,
4495 y: cy - track_h * 0.5,
4496 w: active_w,
4497 h: track_h,
4498 },
4499 brush: Brush::Solid(mul_c(act_trk)),
4500 radius: [corner; 4],
4501 });
4502 }
4503 let tick_start = track_x + corner;
4504 let tick_end = track_x + track_w - corner;
4505 for (i, &tf) in tick_frac.iter().enumerate() {
4506 let tx = tick_start + tf * (tick_end - tick_start);
4507 if i == 0 || i == tick_frac.len() - 1 {
4509 continue;
4510 }
4511 let in_lgap = tx >= active_l - gap && tx <= active_l + gap;
4512 let in_rgap = tx >= active_r - gap && tx <= active_r + gap;
4513 if in_lgap || in_rgap {
4514 continue;
4515 }
4516 let on_active = tx >= active_l + gap && tx <= active_r - gap;
4517 scene.nodes.push(SceneNode::Ellipse {
4518 rect: Rect {
4519 x: tx - dot_r,
4520 y: cy - dot_r,
4521 w: dot_r * 2.0,
4522 h: dot_r * 2.0,
4523 },
4524 brush: Brush::Solid(mul_c(if on_active { act_tick } else { inact_tick })),
4525 });
4526 }
4527 let sx0 = track_x + corner;
4529 scene.nodes.push(SceneNode::Ellipse {
4530 rect: Rect {
4531 x: sx0 - dot_r,
4532 y: cy - dot_r,
4533 w: dot_r * 2.0,
4534 h: dot_r * 2.0,
4535 },
4536 brush: Brush::Solid(mul_c(inact_tick)),
4537 });
4538 let sx = track_x + track_w - corner;
4539 scene.nodes.push(SceneNode::Ellipse {
4540 rect: Rect {
4541 x: sx - dot_r,
4542 y: cy - dot_r,
4543 w: dot_r * 2.0,
4544 h: dot_r * 2.0,
4545 },
4546 brush: Brush::Solid(mul_c(inact_tick)),
4547 });
4548 let da = *drag_active_p.borrow();
4549 let at = *active_thumb_p.borrow();
4550 let hv = hovered_sig.get();
4551 let thumbs = [k0, k1];
4552 for (idx, &kx) in thumbs.iter().enumerate() {
4553 let is_active = da && (if idx == 0 { !at } else { at });
4554 let tw = if is_active { thumb_w * 0.5 } else { thumb_w };
4555 scene.nodes.push(SceneNode::Rect {
4556 rect: Rect {
4557 x: kx - tw * 0.5,
4558 y: cy - thumb_h * 0.5,
4559 w: tw,
4560 h: thumb_h,
4561 },
4562 brush: Brush::Solid(mul_c(thumb_col)),
4563 radius: [tw * 0.5; 4],
4564 });
4565 let sc_target = if !is_enabled {
4566 Color::TRANSPARENT
4567 } else if is_active {
4568 sc.pressed
4569 } else if hv {
4570 sc.hovered
4571 } else {
4572 sc.default
4573 };
4574 if sc_target.3 > 0 {
4575 scene.nodes.push(SceneNode::Rect {
4576 rect: Rect {
4577 x: kx - tw * 0.5,
4578 y: cy - thumb_h * 0.5,
4579 w: tw,
4580 h: thumb_h,
4581 },
4582 brush: Brush::Solid(mul_c(sc_target)),
4583 radius: [tw * 0.5; 4],
4584 });
4585 }
4586 }
4587 })
4588 .on_pointer_enter({
4589 let h = hovered.clone();
4590 let en = is_enabled;
4591 move |_pe: PointerEvent| {
4592 if en {
4593 h.set(true);
4594 }
4595 }
4596 })
4597 .on_pointer_leave({
4598 let h = hovered.clone();
4599 move |_pe: PointerEvent| h.set(false)
4600 })
4601 .on_pointer_down({
4602 let oc = oc.clone();
4603 let track_rect = track_rect.clone();
4604 let drag_active = drag_active.clone();
4605 let active_thumb = active_thumb.clone();
4606 let en = is_enabled;
4607 move |pe: PointerEvent| {
4608 if !en {
4609 return;
4610 }
4611 *drag_active.borrow_mut() = true;
4612 let r = *track_rect.borrow();
4613 let v = value_from_x(pe.position.x, r, min, max, step);
4614 let use_end = (v - end).abs() < (v - start).abs();
4615 *active_thumb.borrow_mut() = use_end;
4616 let (a, b) = if use_end {
4617 (start, v.max(start))
4618 } else {
4619 (v.min(end), end)
4620 };
4621 (oc)(a, b);
4622 }
4623 })
4624 .on_pointer_move({
4625 let oc = oc.clone();
4626 let track_rect = track_rect.clone();
4627 let drag_active = drag_active.clone();
4628 let active_thumb = active_thumb.clone();
4629 move |pe: PointerEvent| {
4630 if !*drag_active.borrow() {
4631 return;
4632 }
4633 let r = *track_rect.borrow();
4634 let v = value_from_x(pe.position.x, r, min, max, step);
4635 let use_end = *active_thumb.borrow();
4636 let (a, b) = if use_end {
4637 (start, v.max(start))
4638 } else {
4639 (v.min(end), end)
4640 };
4641 (oc)(a, b);
4642 }
4643 })
4644 .on_pointer_up({
4645 let drag_active = drag_active.clone();
4646 let active_thumb = active_thumb.clone();
4647 move |_pe: PointerEvent| {
4648 *drag_active.borrow_mut() = false;
4649 *active_thumb.borrow_mut() = false;
4650 }
4651 })
4652 .on_scroll({
4653 let oc = oc.clone();
4654 let active_thumb = active_thumb.clone();
4655 let en = is_enabled;
4656 move |d: Vec2| -> Vec2 {
4657 if !en {
4658 return d;
4659 }
4660 let dir = if d.y < -0.5 {
4661 1
4662 } else if d.y > 0.5 {
4663 -1
4664 } else {
4665 0
4666 };
4667 if dir == 0 {
4668 return d;
4669 }
4670 let step_val = step.unwrap_or(1.0).max(1e-6);
4671 let use_end = *active_thumb.borrow();
4672 let (mut a, mut b) = (start, end);
4673 if use_end {
4674 b = snap_step(end + (dir as f32) * step_val, min, max, step).max(a);
4675 } else {
4676 a = snap_step(start + (dir as f32) * step_val, min, max, step).min(b);
4677 }
4678 if (a - start).abs() > 1e-6 || (b - end).abs() > 1e-6 {
4679 (oc)(a, b);
4680 Vec2 { x: d.x, y: 0.0 }
4681 } else {
4682 d
4683 }
4684 }
4685 })
4686 .then(config.modifier))
4687 .semantics(Semantics {
4688 role: Role::Slider,
4689 label: None,
4690 focused: false,
4691 enabled: is_enabled,
4692 selectable_group: false,
4693 })
4694}
4695
4696#[derive(Clone, Debug)]
4698pub struct CardConfig {
4699 pub modifier: Modifier,
4700 pub enabled: bool,
4702 pub container_color: Color,
4703 pub content_color: Color,
4704 pub disabled_container_color: Color,
4705 pub disabled_content_color: Color,
4706 pub shape_radius: f32,
4707 pub tonal_elevation: f32,
4708 pub state_elevation: Option<StateElevation>,
4709 pub border: Option<(f32, Color)>,
4710 pub interaction_source: Option<MutableInteractionSource>,
4711}
4712
4713impl Default for CardConfig {
4714 fn default() -> Self {
4715 Self {
4716 modifier: Modifier::new(),
4717 enabled: true,
4718 container_color: CardDefaults::filled_container_color(),
4719 content_color: CardDefaults::filled_content_color(),
4720 disabled_container_color: CardDefaults::disabled_container_color(),
4721 disabled_content_color: CardDefaults::disabled_content_color(),
4722 shape_radius: CardDefaults::SHAPE_RADIUS,
4723 tonal_elevation: CardDefaults::ELEVATION,
4724 state_elevation: None,
4725 border: None,
4726 interaction_source: None,
4727 }
4728 }
4729}
4730
4731pub fn Card(config: CardConfig, content: impl FnOnce() -> View) -> View {
4733 let bg = if !config.enabled {
4734 config.disabled_container_color
4735 } else {
4736 config.container_color
4737 };
4738 let fg = if !config.enabled {
4739 config.disabled_content_color
4740 } else {
4741 config.content_color
4742 };
4743 let source: Rc<MutableInteractionSource> = config
4744 .interaction_source
4745 .clone()
4746 .map(Rc::new)
4747 .unwrap_or_else(|| remember(MutableInteractionSource::new));
4748 let mut m = Modifier::new()
4749 .background(bg)
4750 .clip_rounded(config.shape_radius)
4751 .interaction_source(&*source)
4752 .then(config.modifier);
4753 if let Some((w, c)) = config.border {
4754 m = m.border(w, c, config.shape_radius);
4755 }
4756 if let Some(se) = config.state_elevation {
4757 m = m.state_elevation(se);
4758 } else if config.tonal_elevation > 0.0 {
4759 m = m.state_elevation(StateElevation {
4760 default: config.tonal_elevation,
4761 hovered: config.tonal_elevation,
4762 pressed: config.tonal_elevation,
4763 disabled: 0.0,
4764 });
4765 }
4766 Box(m).color(fg).child(content())
4767}
4768
4769pub fn ElevatedCard(config: CardConfig, content: impl FnOnce() -> View) -> View {
4771 let th = theme();
4772 Card(
4773 CardConfig {
4774 container_color: CardDefaults::elevated_container_color(),
4775 state_elevation: Some(StateElevation {
4776 default: th.elevation.level1,
4777 hovered: th.elevation.level2,
4778 pressed: th.elevation.level3,
4779 disabled: 0.0,
4780 }),
4781 ..config
4782 },
4783 content,
4784 )
4785}
4786
4787pub fn OutlinedCard(config: CardConfig, content: impl FnOnce() -> View) -> View {
4789 Card(
4790 CardConfig {
4791 container_color: CardDefaults::outlined_container_color(),
4792 border: Some((1.0, CardDefaults::outlined_border_color())),
4793 ..config
4794 },
4795 content,
4796 )
4797}
4798
4799fn card_state_colors(bg: Color) -> StateColors {
4800 let th = theme();
4801 StateColors {
4802 default: Color::TRANSPARENT,
4803 hovered: th.on_surface.with_alpha_f32(0.08).composite_over(bg),
4804 pressed: th.on_surface.with_alpha_f32(0.12).composite_over(bg),
4805 disabled: th.on_surface.with_alpha_f32(0.12).composite_over(bg),
4806 }
4807}
4808
4809fn clickable_card_impl(
4810 on_click: impl Fn() + 'static,
4811 modifier: Modifier,
4812 bg: Color,
4813 shape_radius: f32,
4814 config: CardConfig,
4815 content: impl FnOnce() -> View,
4816) -> View {
4817 let m = modifier
4818 .state_colors(card_state_colors(bg))
4819 .clickable()
4820 .on_pointer_down({
4821 let cb = on_click;
4822 let en = config.enabled;
4823 move |_| {
4824 if en {
4825 cb();
4826 }
4827 }
4828 });
4829 Card(
4830 CardConfig {
4831 modifier: m,
4832 enabled: config.enabled,
4833 container_color: bg,
4834 content_color: config.content_color,
4835 disabled_container_color: config.disabled_container_color,
4836 disabled_content_color: config.disabled_content_color,
4837 shape_radius,
4838 border: config.border,
4839 state_elevation: config.state_elevation,
4840 tonal_elevation: config.tonal_elevation,
4841 interaction_source: config.interaction_source.clone(),
4842 },
4843 || Column(Modifier::new().fill_max_size()).child(content()),
4844 )
4845}
4846
4847pub fn ClickableCard(
4849 on_click: impl Fn() + 'static,
4850 modifier: Modifier,
4851 config: CardConfig,
4852 content: impl FnOnce() -> View,
4853) -> View {
4854 let th = theme();
4855 clickable_card_impl(
4856 on_click,
4857 modifier,
4858 th.surface_container_highest,
4859 th.shapes.medium,
4860 config,
4861 content,
4862 )
4863}
4864
4865pub fn ClickableElevatedCard(
4867 on_click: impl Fn() + 'static,
4868 modifier: Modifier,
4869 config: CardConfig,
4870 content: impl FnOnce() -> View,
4871) -> View {
4872 let th = theme();
4873 let cfg = CardConfig {
4874 state_elevation: Some(StateElevation {
4875 default: th.elevation.level1,
4876 hovered: th.elevation.level2,
4877 pressed: th.elevation.level3,
4878 disabled: 0.0,
4879 }),
4880 ..config
4881 };
4882 clickable_card_impl(
4883 on_click,
4884 modifier,
4885 th.surface,
4886 th.shapes.medium,
4887 cfg,
4888 content,
4889 )
4890}
4891
4892pub fn ClickableOutlinedCard(
4894 on_click: impl Fn() + 'static,
4895 modifier: Modifier,
4896 config: CardConfig,
4897 content: impl FnOnce() -> View,
4898) -> View {
4899 let th = theme();
4900 let cfg = CardConfig {
4901 border: Some((1.0, th.outline_variant)),
4902 ..config
4903 };
4904 clickable_card_impl(
4905 on_click,
4906 modifier,
4907 th.surface,
4908 th.shapes.medium,
4909 cfg,
4910 content,
4911 )
4912}
4913
4914#[derive(Clone, Debug)]
4916pub struct SnackbarConfig {
4917 pub modifier: Modifier,
4918 pub container_color: Color,
4919 pub content_color: Color,
4920 pub action_color: Color,
4921 pub dismiss_action_content_color: Color,
4922 pub action_on_new_line: bool,
4923 pub shape_radius: f32,
4924 pub min_height: f32,
4925 pub min_width: f32,
4926 pub max_width: f32,
4927}
4928
4929impl Default for SnackbarConfig {
4930 fn default() -> Self {
4931 Self {
4932 modifier: Modifier::new(),
4933 container_color: SnackbarDefaults::container_color(),
4934 content_color: SnackbarDefaults::content_color(),
4935 action_color: SnackbarDefaults::action_color(),
4936 dismiss_action_content_color: SnackbarDefaults::dismiss_action_content_color(),
4937 action_on_new_line: false,
4938 shape_radius: SnackbarDefaults::SHAPE_RADIUS,
4939 min_height: SnackbarDefaults::MIN_HEIGHT,
4940 min_width: SnackbarDefaults::MIN_WIDTH,
4941 max_width: SnackbarDefaults::MAX_WIDTH,
4942 }
4943 }
4944}
4945
4946#[derive(Clone, Copy, Debug)]
4948pub struct ChipColors {
4949 pub container_color: Color,
4950 pub label_color: Color,
4951 pub leading_icon_color: Color,
4952 pub trailing_icon_color: Color,
4953 pub disabled_container_color: Color,
4954 pub disabled_label_color: Color,
4955 pub disabled_leading_icon_color: Color,
4956 pub disabled_trailing_icon_color: Color,
4957 pub selected_container_color: Color,
4958 pub selected_label_color: Color,
4959 pub selected_leading_icon_color: Color,
4960 pub selected_trailing_icon_color: Color,
4961 pub disabled_selected_container_color: Color,
4962}
4963
4964impl ChipColors {
4965 pub fn container(&self, enabled: bool, selected: bool) -> Color {
4966 match (enabled, selected) {
4967 (true, true) => self.selected_container_color,
4968 (true, false) => self.container_color,
4969 (false, true) => self.disabled_selected_container_color,
4970 (false, false) => self.disabled_container_color,
4971 }
4972 }
4973 pub fn label(&self, enabled: bool, selected: bool) -> Color {
4974 if !enabled {
4975 self.disabled_label_color
4976 } else if selected {
4977 self.selected_label_color
4978 } else {
4979 self.label_color
4980 }
4981 }
4982 pub fn leading_icon(&self, enabled: bool, selected: bool) -> Color {
4983 if !enabled {
4984 self.disabled_leading_icon_color
4985 } else if selected {
4986 self.selected_leading_icon_color
4987 } else {
4988 self.leading_icon_color
4989 }
4990 }
4991 pub fn trailing_icon(&self, enabled: bool, selected: bool) -> Color {
4992 if !enabled {
4993 self.disabled_trailing_icon_color
4994 } else if selected {
4995 self.selected_trailing_icon_color
4996 } else {
4997 self.trailing_icon_color
4998 }
4999 }
5000}
5001
5002#[derive(Clone, Copy, Debug)]
5004pub struct ChipElevation {
5005 pub default: f32,
5006 pub hovered: f32,
5007 pub focused: f32,
5008 pub pressed: f32,
5009 pub dragged: f32,
5010 pub disabled: f32,
5011}
5012
5013impl ChipElevation {
5014 pub fn to_state_elevation(&self) -> StateElevation {
5015 StateElevation {
5016 default: self.default,
5017 hovered: self.hovered,
5018 pressed: self.pressed,
5019 disabled: self.disabled,
5020 }
5021 }
5022}
5023
5024impl Default for ChipElevation {
5025 fn default() -> Self {
5026 Self {
5027 default: ChipDefaults::elevation_default(),
5028 hovered: ChipDefaults::elevation_hovered(),
5029 focused: ChipDefaults::elevation_focused(),
5030 pressed: ChipDefaults::elevation_pressed(),
5031 dragged: ChipDefaults::elevation_dragged(),
5032 disabled: ChipDefaults::elevation_disabled(),
5033 }
5034 }
5035}
5036
5037#[derive(Clone, Debug)]
5039pub struct ChipConfig {
5040 pub modifier: Modifier,
5041 pub enabled: bool,
5042 pub colors: ChipColors,
5043 pub elevation: ChipElevation,
5044 pub border_width: f32,
5045 pub border_color: Color,
5046 pub selected_border_color: Color,
5047 pub disabled_border_color: Color,
5048 pub disabled_selected_border_color: Color,
5049 pub shape_radius: f32,
5050 pub horizontal_padding: f32,
5051 pub interaction_source: Option<MutableInteractionSource>,
5052}
5053
5054impl Default for ChipConfig {
5055 fn default() -> Self {
5056 Self {
5057 modifier: Modifier::new(),
5058 enabled: true,
5059 colors: ChipColors {
5060 container_color: ChipDefaults::container_color(),
5061 label_color: ChipDefaults::label_color(),
5062 leading_icon_color: ChipDefaults::leading_icon_color(),
5063 trailing_icon_color: ChipDefaults::trailing_icon_color(),
5064 disabled_container_color: ChipDefaults::disabled_container_color(),
5065 disabled_label_color: ChipDefaults::disabled_label_color(),
5066 disabled_leading_icon_color: ChipDefaults::disabled_leading_icon_color(),
5067 disabled_trailing_icon_color: ChipDefaults::disabled_trailing_icon_color(),
5068 selected_container_color: ChipDefaults::selected_container_color(),
5069 selected_label_color: ChipDefaults::selected_label_color(),
5070 selected_leading_icon_color: ChipDefaults::selected_leading_icon_color(),
5071 selected_trailing_icon_color: ChipDefaults::selected_trailing_icon_color(),
5072 disabled_selected_container_color: ChipDefaults::disabled_selected_container_color(
5073 ),
5074 },
5075 elevation: ChipElevation::default(),
5076 border_width: ChipDefaults::BORDER_WIDTH,
5077 border_color: ChipDefaults::border_color(),
5078 selected_border_color: ChipDefaults::selected_border_color(),
5079 disabled_border_color: ChipDefaults::disabled_border_color(),
5080 disabled_selected_border_color: ChipDefaults::disabled_selected_border_color(),
5081 shape_radius: ChipDefaults::SHAPE_RADIUS,
5082 horizontal_padding: ChipDefaults::HORIZONTAL_PADDING,
5083 interaction_source: None,
5084 }
5085 }
5086}
5087
5088pub fn AssistChip(
5090 on_click: impl Fn() + 'static,
5091 label: View,
5092 leading_icon: Option<View>,
5093 trailing_icon: Option<View>,
5094 config: ChipConfig,
5095) -> View {
5096 let th = theme();
5097 let is_enabled = config.enabled;
5098 let colors = &config.colors;
5099 let bg = colors.container(is_enabled, false);
5100 let label_color = colors.label(is_enabled, false);
5101 let leading_color = colors.leading_icon(is_enabled, false);
5102 let trailing_color = colors.trailing_icon(is_enabled, false);
5103 let border = if is_enabled {
5104 config.border_color
5105 } else {
5106 config.disabled_border_color
5107 };
5108 let shape = config.shape_radius;
5109 let ch_source: Rc<MutableInteractionSource> = config
5110 .interaction_source
5111 .clone()
5112 .map(Rc::new)
5113 .unwrap_or_else(|| remember(MutableInteractionSource::new));
5114
5115 let mut m = Modifier::new()
5116 .state_colors(StateColors {
5117 default: Color::TRANSPARENT,
5118 hovered: th.on_surface.with_alpha_f32(0.08),
5119 pressed: th.on_surface.with_alpha_f32(0.12),
5120 disabled: Color::TRANSPARENT,
5121 })
5122 .padding_values(PaddingValues {
5123 left: config.horizontal_padding,
5124 right: config.horizontal_padding,
5125 top: 8.0,
5126 bottom: 8.0,
5127 })
5128 .background(bg)
5129 .clip_rounded(shape)
5130 .interaction_source(&*ch_source)
5131 .then(config.modifier);
5132
5133 if config.border_width > 0.0 && border != Color::TRANSPARENT {
5134 m = m.border(config.border_width, border, shape);
5135 }
5136
5137 if is_enabled {
5138 m = m.clickable().on_click(move || on_click());
5139 }
5140
5141 Box(m).child(
5142 Row(Modifier::new().align_items(AlignItems::CENTER)).child((
5143 leading_icon
5144 .map(|v| {
5145 Box(Modifier::new().padding_values(PaddingValues {
5146 left: 0.0,
5147 right: 8.0,
5148 top: 0.0,
5149 bottom: 0.0,
5150 }))
5151 .child(with_content_color(leading_color, move || v))
5152 })
5153 .unwrap_or(Box(Modifier::new())),
5154 with_content_color(label_color, move || label),
5155 trailing_icon
5156 .map(|v| {
5157 Box(Modifier::new().padding_values(PaddingValues {
5158 left: 8.0,
5159 right: 0.0,
5160 top: 0.0,
5161 bottom: 0.0,
5162 }))
5163 .child(with_content_color(trailing_color, move || v))
5164 })
5165 .unwrap_or(Box(Modifier::new())),
5166 )),
5167 )
5168}
5169
5170pub fn ElevatedAssistChip(
5172 on_click: impl Fn() + 'static,
5173 label: View,
5174 leading_icon: Option<View>,
5175 trailing_icon: Option<View>,
5176 config: ChipConfig,
5177) -> View {
5178 let th = theme();
5179 let is_enabled = config.enabled;
5180 let colors = &config.colors;
5181 let bg = colors.container(is_enabled, false);
5182 let label_color = colors.label(is_enabled, false);
5183 let leading_color = colors.leading_icon(is_enabled, false);
5184 let trailing_color = colors.trailing_icon(is_enabled, false);
5185 let shape = config.shape_radius;
5186
5187 let mut m = Modifier::new()
5188 .state_colors(StateColors {
5189 default: Color::TRANSPARENT,
5190 hovered: th.on_surface.with_alpha_f32(0.08),
5191 pressed: th.on_surface.with_alpha_f32(0.12),
5192 disabled: Color::TRANSPARENT,
5193 })
5194 .state_elevation(config.elevation.to_state_elevation())
5195 .padding_values(PaddingValues {
5196 left: config.horizontal_padding,
5197 right: config.horizontal_padding,
5198 top: 8.0,
5199 bottom: 8.0,
5200 })
5201 .background(bg)
5202 .clip_rounded(shape)
5203 .then(config.modifier);
5204
5205 if is_enabled {
5206 m = m.clickable().on_click(move || on_click());
5207 }
5208
5209 Box(m).child(
5210 Row(Modifier::new().align_items(AlignItems::CENTER)).child((
5211 leading_icon
5212 .map(|v| {
5213 Box(Modifier::new().padding_values(PaddingValues {
5214 left: 0.0,
5215 right: 8.0,
5216 top: 0.0,
5217 bottom: 0.0,
5218 }))
5219 .child(with_content_color(leading_color, move || v))
5220 })
5221 .unwrap_or(Box(Modifier::new())),
5222 with_content_color(label_color, move || label),
5223 trailing_icon
5224 .map(|v| {
5225 Box(Modifier::new().padding_values(PaddingValues {
5226 left: 8.0,
5227 right: 0.0,
5228 top: 0.0,
5229 bottom: 0.0,
5230 }))
5231 .child(with_content_color(trailing_color, move || v))
5232 })
5233 .unwrap_or(Box(Modifier::new())),
5234 )),
5235 )
5236}
5237
5238#[derive(Clone, Debug)]
5240pub struct NavigationBarConfig {
5241 pub modifier: Modifier,
5242 pub container_color: Color,
5243 pub content_color: Color,
5244 pub selected_icon_color: Color,
5245 pub selected_text_color: Color,
5246 pub unselected_icon_color: Color,
5247 pub unselected_text_color: Color,
5248 pub indicator_color: Color,
5249 pub height: f32,
5250 pub tonal_elevation: f32,
5251 pub indicator_opacity: f32,
5252 pub indicator_radius: f32,
5253 pub item_spacing: f32,
5254 pub indicator_width: f32,
5255 pub indicator_height: f32,
5256}
5257
5258impl Default for NavigationBarConfig {
5259 fn default() -> Self {
5260 Self {
5261 modifier: Modifier::new(),
5262 container_color: NavigationBarDefaults::container_color(),
5263 content_color: NavigationBarDefaults::content_color(),
5264 selected_icon_color: NavigationBarDefaults::selected_icon_color(),
5265 selected_text_color: NavigationBarDefaults::selected_text_color(),
5266 unselected_icon_color: NavigationBarDefaults::unselected_icon_color(),
5267 unselected_text_color: NavigationBarDefaults::unselected_text_color(),
5268 indicator_color: NavigationBarDefaults::indicator_color(),
5269 height: NavigationBarDefaults::HEIGHT,
5270 tonal_elevation: NavigationBarDefaults::TONAL_ELEVATION,
5271 indicator_opacity: NavigationBarDefaults::ITEM_ACTIVE_INDICATOR_OPACITY,
5272 indicator_radius: NavigationBarDefaults::INDICATOR_RADIUS,
5273 item_spacing: NavigationBarDefaults::ITEM_SPACING,
5274 indicator_width: NavigationBarDefaults::ACTIVE_INDICATOR_WIDTH,
5275 indicator_height: NavigationBarDefaults::ACTIVE_INDICATOR_HEIGHT,
5276 }
5277 }
5278}
5279
5280#[derive(Clone, Debug)]
5282pub struct NavigationRailConfig {
5283 pub modifier: Modifier,
5284 pub container_color: Color,
5285 pub selected_icon_color: Color,
5286 pub selected_text_color: Color,
5287 pub unselected_icon_color: Color,
5288 pub unselected_text_color: Color,
5289 pub indicator_color: Color,
5290 pub width: f32,
5291 pub item_radius: f32,
5292 pub indicator_opacity: f32,
5293 pub item_spacing: f32,
5294 pub indicator_width: f32,
5295 pub indicator_height: f32,
5296}
5297
5298impl Default for NavigationRailConfig {
5299 fn default() -> Self {
5300 Self {
5301 modifier: Modifier::new(),
5302 container_color: NavigationRailDefaults::container_color(),
5303 selected_icon_color: NavigationRailDefaults::selected_icon_color(),
5304 selected_text_color: NavigationRailDefaults::selected_text_color(),
5305 unselected_icon_color: NavigationRailDefaults::unselected_icon_color(),
5306 unselected_text_color: NavigationRailDefaults::unselected_text_color(),
5307 indicator_color: NavigationRailDefaults::indicator_color(),
5308 width: NavigationRailDefaults::WIDTH,
5309 item_radius: NavigationRailDefaults::ITEM_RADIUS,
5310 indicator_opacity: NavigationRailDefaults::ITEM_ACTIVE_INDICATOR_OPACITY,
5311 item_spacing: NavigationRailDefaults::ITEM_SPACING,
5312 indicator_width: NavigationRailDefaults::ACTIVE_INDICATOR_WIDTH,
5313 indicator_height: NavigationRailDefaults::ACTIVE_INDICATOR_HEIGHT,
5314 }
5315 }
5316}
5317
5318#[derive(Clone, Debug)]
5320pub struct ScaffoldConfig {
5321 pub modifier: Modifier,
5322 pub container_color: Color,
5323 pub top_bar_height: f32,
5324 pub bottom_bar_height: f32,
5325 pub fab_margin: f32,
5326}
5327
5328impl Default for ScaffoldConfig {
5329 fn default() -> Self {
5330 Self {
5331 modifier: Modifier::new(),
5332 container_color: ScaffoldDefaults::container_color(),
5333 top_bar_height: ScaffoldDefaults::TOP_BAR_HEIGHT,
5334 bottom_bar_height: ScaffoldDefaults::BOTTOM_BAR_HEIGHT,
5335 fab_margin: ScaffoldDefaults::FAB_MARGIN,
5336 }
5337 }
5338}
5339
5340#[derive(Clone, Debug)]
5342pub struct NavigationDrawerConfig {
5343 pub modifier: Modifier,
5344 pub container_color: Color,
5345 pub content_color: Color,
5346 pub scrim_color: Color,
5347 pub tonal_elevation: f32,
5348 pub width: f32,
5349 pub shape_radius: f32,
5350}
5351
5352impl Default for NavigationDrawerConfig {
5353 fn default() -> Self {
5354 Self {
5355 modifier: Modifier::new(),
5356 container_color: NavigationDrawerDefaults::container_color(),
5357 content_color: NavigationDrawerDefaults::content_color(),
5358 scrim_color: NavigationDrawerDefaults::scrim_color(),
5359 tonal_elevation: NavigationDrawerDefaults::TONAL_ELEVATION,
5360 width: NavigationDrawerDefaults::WIDTH,
5361 shape_radius: NavigationDrawerDefaults::SHAPE_RADIUS,
5362 }
5363 }
5364}
5365
5366#[derive(Clone, Debug)]
5368pub struct BottomSheetConfig {
5369 pub modifier: Modifier,
5370 pub container_color: Color,
5371 pub content_color: Color,
5372 pub scrim_color: Color,
5373 pub tonal_elevation: f32,
5374 pub shadow_elevation: f32,
5375 pub drag_handle_color: Color,
5376 pub shape_radius: f32,
5377 pub max_width: f32,
5378 pub drag_handle_width: f32,
5379 pub drag_handle_height: f32,
5380 pub peek_height: f32,
5381 pub gestures_enabled: bool,
5382}
5383
5384impl Default for BottomSheetConfig {
5385 fn default() -> Self {
5386 Self {
5387 modifier: Modifier::new(),
5388 container_color: BottomSheetDefaults::container_color(),
5389 content_color: BottomSheetDefaults::content_color(),
5390 scrim_color: BottomSheetDefaults::scrim_color(),
5391 tonal_elevation: BottomSheetDefaults::TONAL_ELEVATION,
5392 shadow_elevation: 0.0,
5393 drag_handle_color: BottomSheetDefaults::drag_handle_color(),
5394 shape_radius: BottomSheetDefaults::SHAPE_RADIUS,
5395 max_width: BottomSheetDefaults::MAX_WIDTH,
5396 drag_handle_width: BottomSheetDefaults::DRAG_HANDLE_WIDTH,
5397 drag_handle_height: BottomSheetDefaults::DRAG_HANDLE_HEIGHT,
5398 peek_height: BottomSheetDefaults::PEEK_HEIGHT,
5399 gestures_enabled: true,
5400 }
5401 }
5402}
5403
5404#[derive(Clone, Copy, Debug)]
5406pub struct SearchBarColors {
5407 pub container_color: Color,
5408 pub active_container_color: Color,
5409 pub divider_color: Color,
5410 pub content_color: Color,
5411 pub placeholder_color: Color,
5412 pub scrim_color: Color,
5413}
5414
5415impl SearchBarColors {
5416 pub fn container(&self, active: bool) -> Color {
5417 if active {
5418 self.active_container_color
5419 } else {
5420 self.container_color
5421 }
5422 }
5423}
5424
5425impl Default for SearchBarColors {
5426 fn default() -> Self {
5427 Self {
5428 container_color: SearchBarDefaults::container_color(),
5429 active_container_color: SearchBarDefaults::active_container_color(),
5430 divider_color: SearchBarDefaults::divider_color(),
5431 content_color: SearchBarDefaults::content_color(),
5432 placeholder_color: SearchBarDefaults::placeholder_color(),
5433 scrim_color: SearchBarDefaults::scrim_color(),
5434 }
5435 }
5436}
5437
5438#[derive(Clone, Copy, Debug)]
5440pub struct AppBarWithSearchColors {
5441 pub search_bar_colors: SearchBarColors,
5442 pub scrolled_search_bar_container_color: Color,
5443 pub app_bar_container_color: Color,
5444 pub scrolled_app_bar_container_color: Color,
5445 pub navigation_icon_content_color: Color,
5446 pub action_icon_content_color: Color,
5447}
5448
5449impl AppBarWithSearchColors {
5450 pub fn search_bar_container(&self, scroll_fraction: f32) -> Color {
5451 lerp_color(
5452 self.search_bar_colors.container_color,
5453 self.scrolled_search_bar_container_color,
5454 scroll_fraction.clamp(0.0, 1.0),
5455 )
5456 }
5457 pub fn app_bar_container(&self, scroll_fraction: f32) -> Color {
5458 lerp_color(
5459 self.app_bar_container_color,
5460 self.scrolled_app_bar_container_color,
5461 scroll_fraction.clamp(0.0, 1.0),
5462 )
5463 }
5464}
5465
5466impl Default for AppBarWithSearchColors {
5467 fn default() -> Self {
5468 Self {
5469 search_bar_colors: SearchBarColors::default(),
5470 scrolled_search_bar_container_color: SearchBarDefaults::scrolled_container_color(),
5471 app_bar_container_color: SearchBarDefaults::app_bar_container_color(),
5472 scrolled_app_bar_container_color: SearchBarDefaults::scrolled_app_bar_container_color(),
5473 navigation_icon_content_color: SearchBarDefaults::navigation_icon_content_color(),
5474 action_icon_content_color: SearchBarDefaults::action_icon_content_color(),
5475 }
5476 }
5477}
5478
5479#[derive(Clone, Debug)]
5481pub struct SearchBarConfig {
5482 pub modifier: Modifier,
5483 pub colors: SearchBarColors,
5484 pub height: f32,
5485 pub shape_radius: f32,
5486 pub active_shape_radius: f32,
5487 pub expanded_width: f32,
5488 pub collapsed_width: f32,
5489 pub tonal_elevation: f32,
5490 pub shadow_elevation: f32,
5491 pub window_insets: WindowInsets,
5492 pub content_padding: PaddingValues,
5493 pub min_width: f32,
5494 pub max_width: f32,
5495}
5496
5497impl Default for SearchBarConfig {
5498 fn default() -> Self {
5499 Self {
5500 modifier: Modifier::new(),
5501 colors: SearchBarColors::default(),
5502 height: SearchBarDefaults::HEIGHT,
5503 shape_radius: SearchBarDefaults::SHAPE_RADIUS,
5504 active_shape_radius: SearchBarDefaults::ACTIVE_SHAPE_RADIUS,
5505 expanded_width: SearchBarDefaults::EXPANDED_WIDTH,
5506 collapsed_width: SearchBarDefaults::COLLAPSED_WIDTH,
5507 tonal_elevation: SearchBarDefaults::TONAL_ELEVATION,
5508 shadow_elevation: SearchBarDefaults::SHADOW_ELEVATION,
5509 window_insets: WindowInsets::default(),
5510 content_padding: SearchBarDefaults::CONTENT_PADDING,
5511 min_width: SearchBarDefaults::MIN_WIDTH,
5512 max_width: SearchBarDefaults::MAX_WIDTH,
5513 }
5514 }
5515}
5516
5517#[derive(Clone, Debug)]
5519pub struct ExpandedFullScreenSearchBarConfig {
5520 pub modifier: Modifier,
5521 pub colors: SearchBarColors,
5522 pub collapsed_shape_radius: f32,
5523 pub tonal_elevation: f32,
5524 pub shadow_elevation: f32,
5525 pub window_insets: WindowInsets,
5526 pub scrim_color: Color,
5527}
5528
5529impl Default for ExpandedFullScreenSearchBarConfig {
5530 fn default() -> Self {
5531 Self {
5532 modifier: Modifier::new(),
5533 colors: SearchBarColors::default(),
5534 collapsed_shape_radius: SearchBarDefaults::SHAPE_RADIUS,
5535 tonal_elevation: SearchBarDefaults::TONAL_ELEVATION,
5536 shadow_elevation: SearchBarDefaults::SHADOW_ELEVATION,
5537 window_insets: WindowInsets::default(),
5538 scrim_color: SearchBarDefaults::scrim_color(),
5539 }
5540 }
5541}
5542
5543#[derive(Clone, Debug)]
5545pub struct ExpandedDockedSearchBarConfig {
5546 pub modifier: Modifier,
5547 pub colors: SearchBarColors,
5548 pub shape_radius: f32,
5549 pub dropdown_shape_radius: f32,
5550 pub dropdown_gap_size: f32,
5551 pub dropdown_scrim_color: Color,
5552 pub tonal_elevation: f32,
5553 pub shadow_elevation: f32,
5554}
5555
5556impl Default for ExpandedDockedSearchBarConfig {
5557 fn default() -> Self {
5558 Self {
5559 modifier: Modifier::new(),
5560 colors: SearchBarColors::default(),
5561 shape_radius: SearchBarDefaults::DOCKED_SHAPE_RADIUS,
5562 dropdown_shape_radius: SearchBarDefaults::DROPDOWN_SHAPE_RADIUS,
5563 dropdown_gap_size: SearchBarDefaults::DROPDOWN_GAP_SIZE,
5564 dropdown_scrim_color: SearchBarDefaults::dropdown_scrim_color(),
5565 tonal_elevation: SearchBarDefaults::TONAL_ELEVATION,
5566 shadow_elevation: SearchBarDefaults::SHADOW_ELEVATION,
5567 }
5568 }
5569}
5570
5571#[derive(Clone, Debug)]
5573pub struct AppBarWithSearchConfig {
5574 pub modifier: Modifier,
5575 pub colors: AppBarWithSearchColors,
5576 pub height: f32,
5577 pub shape_radius: f32,
5578 pub tonal_elevation: f32,
5579 pub shadow_elevation: f32,
5580 pub content_padding: PaddingValues,
5581 pub window_insets: WindowInsets,
5582 pub scroll_fraction: f32,
5583 pub scroll_offset: f32,
5584}
5585
5586impl Default for AppBarWithSearchConfig {
5587 fn default() -> Self {
5588 Self {
5589 modifier: Modifier::new(),
5590 colors: AppBarWithSearchColors::default(),
5591 height: SearchBarDefaults::HEIGHT,
5592 shape_radius: SearchBarDefaults::SHAPE_RADIUS,
5593 tonal_elevation: SearchBarDefaults::TONAL_ELEVATION,
5594 shadow_elevation: SearchBarDefaults::SHADOW_ELEVATION,
5595 content_padding: SearchBarDefaults::CONTENT_PADDING,
5596 window_insets: WindowInsets::default(),
5597 scroll_fraction: 0.0,
5598 scroll_offset: 0.0,
5599 }
5600 }
5601}
5602
5603pub struct SearchBarScrollBehavior {
5605 pub collapsed_offset: Signal<f32>,
5606 pub height: f32,
5607 pub collapsed_height: f32,
5608 _pending: Rc<Cell<f32>>,
5609}
5610
5611impl SearchBarScrollBehavior {
5612 pub fn new(height: f32, collapsed_height: f32) -> Self {
5613 Self {
5614 collapsed_offset: signal(0.0),
5615 height,
5616 collapsed_height,
5617 _pending: Rc::new(Cell::new(0.0)),
5618 }
5619 }
5620
5621 pub fn offset(&self) -> f32 {
5622 self.collapsed_offset.get()
5623 }
5624
5625 pub fn nested_scroll_connection(&self) -> NestedScrollConnection {
5626 let offset = self.collapsed_offset.clone();
5627 let max_offset = self.height - self.collapsed_height;
5628 NestedScrollConnection::new().on_pre_scroll(move |delta: Vec2, _source| {
5629 let cur = offset.get();
5630 let new = (cur - delta.y).clamp(-max_offset, 0.0);
5631 let consumed = cur - new;
5632 offset.set(new);
5633 request_frame();
5634 Vec2 {
5635 x: 0.0,
5636 y: consumed,
5637 }
5638 })
5639 }
5640}
5641
5642#[derive(Clone, Debug)]
5644pub struct DropdownMenuConfig {
5645 pub modifier: Modifier,
5646 pub container_color: Color,
5647 pub item_text_color: Color,
5648 pub disabled_item_text_color: Color,
5649 pub divider_color: Color,
5650 pub min_width: f32,
5651 pub item_height: f32,
5652 pub shadow_elevation: Option<f32>,
5653 pub tonal_elevation: f32,
5654 pub border: Option<(f32, Color, f32)>,
5655 pub shape_radius: Option<f32>,
5656 pub offset_x: f32,
5657 pub offset_y: f32,
5658}
5659
5660impl Default for DropdownMenuConfig {
5661 fn default() -> Self {
5662 Self {
5663 modifier: Modifier::new(),
5664 container_color: DropdownMenuDefaults::container_color(),
5665 item_text_color: DropdownMenuDefaults::item_text_color(),
5666 disabled_item_text_color: DropdownMenuDefaults::disabled_item_text_color(),
5667 divider_color: DropdownMenuDefaults::divider_color(),
5668 min_width: DropdownMenuDefaults::MIN_WIDTH,
5669 item_height: DropdownMenuDefaults::ITEM_HEIGHT,
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 }
5677 }
5678}
5679
5680#[derive(Clone, Debug)]
5682pub struct TooltipConfig {
5683 pub modifier: Modifier,
5684 pub container_color: Color,
5685 pub content_color: Color,
5686 pub offset_y: f32,
5687 pub horizontal_padding: f32,
5688 pub vertical_padding: f32,
5689 pub has_action: bool,
5690 pub enable_user_input: bool,
5691 pub focusable: bool,
5692 pub max_width: f32,
5693 pub tonal_elevation: f32,
5694 pub shadow_elevation: f32,
5695}
5696
5697impl Default for TooltipConfig {
5698 fn default() -> Self {
5699 Self {
5700 modifier: Modifier::new(),
5701 container_color: TooltipDefaults::container_color(),
5702 content_color: TooltipDefaults::content_color(),
5703 offset_y: TooltipDefaults::OFFSET_Y,
5704 horizontal_padding: TooltipDefaults::HORIZONTAL_PADDING,
5705 vertical_padding: TooltipDefaults::VERTICAL_PADDING,
5706 has_action: false,
5707 enable_user_input: true,
5708 focusable: false,
5709 max_width: TooltipDefaults::MAX_WIDTH,
5710 tonal_elevation: 0.0,
5711 shadow_elevation: 0.0,
5712 }
5713 }
5714}
5715
5716#[derive(Clone, Debug)]
5718pub struct SwipeToDismissConfig {
5719 pub modifier: Modifier,
5720 pub dismiss_threshold: f32,
5721 pub dismissed_offset: f32,
5722 pub animation_spec: AnimationSpec,
5723 pub gestures_enabled: bool,
5724 pub enable_dismiss_from_start_to_end: bool,
5725 pub enable_dismiss_from_end_to_start: bool,
5726}
5727
5728impl Default for SwipeToDismissConfig {
5729 fn default() -> Self {
5730 Self {
5731 modifier: Modifier::new(),
5732 dismiss_threshold: SwipeToDismissDefaults::DISMISS_THRESHOLD,
5733 dismissed_offset: SwipeToDismissDefaults::DISMISSED_OFFSET,
5734 animation_spec: AnimationSpec::spring_gentle(),
5735 gestures_enabled: true,
5736 enable_dismiss_from_start_to_end: true,
5737 enable_dismiss_from_end_to_start: true,
5738 }
5739 }
5740}
5741
5742#[derive(Clone, Debug)]
5744pub struct PullToRefreshConfig {
5745 pub modifier: Modifier,
5746 pub indicator_color: Color,
5747 pub threshold: f32,
5748 pub content_alignment: AlignItems,
5749}
5750
5751impl Default for PullToRefreshConfig {
5752 fn default() -> Self {
5753 Self {
5754 modifier: Modifier::new(),
5755 indicator_color: PullToRefreshDefaults::indicator_color(),
5756 threshold: PullToRefreshDefaults::THRESHOLD,
5757 content_alignment: AlignItems::FLEX_START,
5758 }
5759 }
5760}