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