Skip to main content

repose_material/material3/
components.rs

1#![allow(non_snake_case)]
2
3use std::cell::Cell;
4use std::rc::Rc;
5use std::sync::atomic::{AtomicU64, Ordering};
6
7use repose_core::*;
8use repose_ui::anim::{animate_color, animate_f32};
9use repose_ui::{Box, Column, Row, Stack, Text, TextStyle, ViewExt};
10
11use super::*;
12
13use crate::{Icon, Symbol};
14
15/// Configuration for [`TopAppBar`].
16#[derive(Clone, Debug)]
17pub struct TopAppBarConfig {
18    pub modifier: Modifier,
19    pub container_color: Color,
20    pub title_color: Color,
21    pub height: f32,
22}
23
24impl Default for TopAppBarConfig {
25    fn default() -> Self {
26        Self {
27            modifier: Modifier::new(),
28            container_color: TopAppBarDefaults::container_color(),
29            title_color: TopAppBarDefaults::title_color(),
30            height: TopAppBarDefaults::HEIGHT,
31        }
32    }
33}
34
35/// M3 Top App Bar (small). Displays a title with optional navigation icon and
36/// trailing action buttons.
37pub fn TopAppBar(
38    title: impl Into<String>,
39    navigation_icon: Option<View>,
40    actions: Vec<View>,
41    config: TopAppBarConfig,
42) -> View {
43    let th = theme();
44    Row(Modifier::new()
45        .min_width(200.0)
46        .height(config.height)
47        .background(config.container_color)
48        .padding_values(PaddingValues {
49            left: 4.0,
50            right: 4.0,
51            top: 0.0,
52            bottom: 0.0,
53        })
54        .align_items(AlignItems::Center)
55        .then(config.modifier))
56    .child((
57        navigation_icon.unwrap_or(Box(Modifier::new().width(16.0).fill_max_height())),
58        Box(Modifier::new()
59            .padding_values(PaddingValues {
60                left: 16.0,
61                right: 0.0,
62                top: 0.0,
63                bottom: 0.0,
64            })
65            .flex_grow(1.0))
66        .child(
67            Text(title)
68                .color(config.title_color)
69                .size(th.typography.title_large),
70        ),
71        Row(Modifier::new().align_items(AlignItems::Center)).child(actions),
72    ))
73}
74
75/// M3 Center-Aligned Top App Bar - same as TopAppBar but title is centered.
76pub fn CenterAlignedTopAppBar(
77    title: impl Into<String>,
78    navigation_icon: Option<View>,
79    actions: Vec<View>,
80    config: TopAppBarConfig,
81) -> View {
82    let th = theme();
83    Row(Modifier::new()
84        .min_width(200.0)
85        .height(config.height)
86        .background(config.container_color)
87        .padding_values(PaddingValues {
88            left: 4.0,
89            right: 4.0,
90            top: 0.0,
91            bottom: 0.0,
92        })
93        .align_items(AlignItems::Center)
94        .justify_content(JustifyContent::Center)
95        .then(config.modifier))
96    .child((
97        navigation_icon.unwrap_or(Box(Modifier::new().width(16.0).fill_max_height())),
98        Box(Modifier::new().flex_grow(1.0)).child(
99            Text(title)
100                .color(config.title_color)
101                .size(th.typography.title_large),
102        ),
103        Row(Modifier::new().align_items(AlignItems::Center)).child(actions),
104    ))
105}
106
107/// Configuration for [`Surface`].
108#[derive(Clone, Debug)]
109pub struct SurfaceConfig {
110    pub modifier: Modifier,
111    pub color: Color,
112    pub shape_radius: f32,
113    pub tonal_elevation: f32,
114    pub border: Option<(f32, Color)>,
115}
116
117impl Default for SurfaceConfig {
118    fn default() -> Self {
119        Self {
120            modifier: Modifier::new(),
121            color: SurfaceDefaults::color(),
122            shape_radius: SurfaceDefaults::SHAPE_RADIUS,
123            tonal_elevation: SurfaceDefaults::TONAL_ELEVATION,
124            border: None,
125        }
126    }
127}
128
129/// M3 Surface - a basic container with shape, color, elevation, and border.
130/// Sets the ContentColor local for children based on the surface color.
131pub fn Surface(config: SurfaceConfig, content: impl FnOnce() -> View) -> View {
132    let mut m = Modifier::new()
133        .background(config.color)
134        .clip_rounded(config.shape_radius)
135        .then(config.modifier);
136    if config.tonal_elevation > 0.0 {
137        m = m.state_elevation(StateElevation {
138            default: config.tonal_elevation,
139            hovered: config.tonal_elevation,
140            pressed: config.tonal_elevation,
141            disabled: 0.0,
142        });
143    }
144    if let Some((w, c)) = config.border {
145        m = m.border(w, c, config.shape_radius);
146    }
147    Box(m).child(content())
148}
149
150/// Configuration for [`IconButton`] and [`FilledIconButton`].
151#[derive(Clone, Debug)]
152pub struct IconButtonConfig {
153    pub modifier: Modifier,
154    pub content_color: Option<Color>,
155    pub container_size: Option<f32>,
156    pub filler_container_color: Option<Color>,
157    pub state_colors: Option<StateColors>,
158}
159
160impl Default for IconButtonConfig {
161    fn default() -> Self {
162        Self {
163            modifier: Modifier::new(),
164            content_color: None,
165            container_size: None,
166            filler_container_color: None,
167            state_colors: None,
168        }
169    }
170}
171
172/// M3 Icon Button - a tappable circular container for an icon.
173pub fn IconButton(icon: View, on_click: impl Fn() + 'static, config: IconButtonConfig) -> View {
174    let sz = config
175        .container_size
176        .unwrap_or(IconButtonDefaults::CONTAINER_SIZE);
177    let colors = config
178        .state_colors
179        .unwrap_or_else(IconButtonDefaults::state_colors_default);
180    Box(Modifier::new()
181        .size(sz, sz)
182        .clip_rounded(sz * 0.5)
183        .state_colors(colors)
184        .align_items(AlignItems::Center)
185        .justify_content(JustifyContent::Center)
186        .clickable()
187        .on_pointer_down(move |_| on_click())
188        .then(config.modifier))
189    .child(icon)
190}
191
192/// M3 Filled Icon Button - icon button with a filled container background.
193pub fn FilledIconButton(
194    icon: View,
195    on_click: impl Fn() + 'static,
196    config: IconButtonConfig,
197) -> View {
198    let th = theme();
199    let content_color = config
200        .content_color
201        .unwrap_or_else(IconButtonDefaults::filled_content_color);
202    let bg = config
203        .filler_container_color
204        .unwrap_or_else(IconButtonDefaults::filled_container_color);
205    let sz = config
206        .container_size
207        .unwrap_or(IconButtonDefaults::FILLED_CONTAINER_SIZE);
208    Box(Modifier::new()
209        .size(sz, sz)
210        .clip_rounded(sz * 0.5)
211        .background(bg)
212        .state_colors(StateColors {
213            default: Color::TRANSPARENT,
214            hovered: content_color.with_alpha_f32(0.08),
215            pressed: content_color.with_alpha_f32(0.12),
216            disabled: th.on_surface.with_alpha_f32(0.12),
217        })
218        .align_items(AlignItems::Center)
219        .justify_content(JustifyContent::Center)
220        .clickable()
221        .on_pointer_down(move |_| on_click())
222        .then(config.modifier))
223    .child(icon)
224}
225
226/// M3 Filled Tonal Icon Button - icon button with a secondary container background.
227pub fn FilledTonalIconButton(
228    icon: View,
229    on_click: impl Fn() + 'static,
230    config: IconButtonConfig,
231) -> View {
232    let th = theme();
233    let content_color = config
234        .content_color
235        .unwrap_or_else(IconButtonDefaults::filled_tonal_content_color);
236    let bg = config
237        .filler_container_color
238        .unwrap_or_else(IconButtonDefaults::filled_tonal_container_color);
239    let sz = config
240        .container_size
241        .unwrap_or(IconButtonDefaults::FILLED_CONTAINER_SIZE);
242    Box(Modifier::new()
243        .size(sz, sz)
244        .clip_rounded(sz * 0.5)
245        .background(bg)
246        .state_colors(StateColors {
247            default: Color::TRANSPARENT,
248            hovered: content_color.with_alpha_f32(0.08),
249            pressed: content_color.with_alpha_f32(0.12),
250            disabled: th.on_surface.with_alpha_f32(0.12),
251        })
252        .align_items(AlignItems::Center)
253        .justify_content(JustifyContent::Center)
254        .clickable()
255        .on_pointer_down(move |_| on_click())
256        .then(config.modifier))
257    .child(icon)
258}
259
260/// M3 Outlined Icon Button - icon button with a transparent background and border.
261pub fn OutlinedIconButton(
262    icon: View,
263    on_click: impl Fn() + 'static,
264    config: IconButtonConfig,
265) -> View {
266    let th = theme();
267    let sz = config
268        .container_size
269        .unwrap_or(IconButtonDefaults::CONTAINER_SIZE);
270    let colors = config
271        .state_colors
272        .unwrap_or_else(IconButtonDefaults::state_colors_default);
273    Box(Modifier::new()
274        .size(sz, sz)
275        .clip_rounded(sz * 0.5)
276        .state_colors(colors)
277        .border(1.0, th.outline, sz * 0.5)
278        .align_items(AlignItems::Center)
279        .justify_content(JustifyContent::Center)
280        .clickable()
281        .on_pointer_down(move |_| on_click())
282        .then(config.modifier))
283    .child(icon)
284}
285
286/// Configuration for button components.
287#[derive(Clone, Debug)]
288pub struct ButtonConfig {
289    pub modifier: Modifier,
290    pub enabled: bool,
291    pub content_color: Option<Color>,
292    pub container_color: Option<Color>,
293    pub state_colors: Option<StateColors>,
294    pub state_elevation: Option<StateElevation>,
295    pub border: Option<(f32, Color, f32)>,
296    pub shape_radius: f32,
297    pub content_padding: Option<PaddingValues>,
298    pub height: f32,
299}
300
301impl Default for ButtonConfig {
302    fn default() -> Self {
303        Self {
304            modifier: Modifier::new(),
305            enabled: true,
306            content_color: None,
307            container_color: None,
308            state_colors: None,
309            state_elevation: None,
310            border: None,
311            shape_radius: ButtonDefaults::SHAPE_RADIUS,
312            content_padding: None,
313            height: ButtonDefaults::HEIGHT,
314        }
315    }
316}
317
318fn button_impl(
319    outer_modifier: Modifier,
320    on_click: impl Fn() + 'static,
321    content: impl FnOnce() -> View,
322    content_color: Color,
323    container_color: Option<Color>,
324    state_colors: StateColors,
325    state_elevation: Option<StateElevation>,
326    border: Option<(f32, Color, f32)>,
327    padding_left: f32,
328    padding_right: f32,
329    height: f32,
330    shape_radius: f32,
331    enabled: bool,
332) -> View {
333    let mut m = Modifier::new().height(height).min_width(48.0);
334    if let Some(bg) = container_color {
335        m = m.background(bg);
336    }
337    m = m.state_colors(state_colors);
338    if let Some(se) = state_elevation {
339        m = m.state_elevation(se);
340    }
341    if let Some((w, c, r)) = border {
342        m = m.border(w, c, r);
343    }
344    m = m
345        .clip_rounded(shape_radius)
346        .padding_values(PaddingValues {
347            left: padding_left,
348            right: padding_right,
349            top: 0.0,
350            bottom: 0.0,
351        })
352        .align_items(AlignItems::Center)
353        .justify_content(JustifyContent::Center);
354    if enabled {
355        m = m.clickable().on_pointer_down(move |_| on_click());
356    }
357    m = m.then(outer_modifier);
358    let content = with_content_color(
359        if enabled {
360            content_color
361        } else {
362            content_color.with_alpha_f32(0.38)
363        },
364        content,
365    );
366    Box(m).child(content)
367}
368
369/// M3 Filled Button - the basic Material3 button (equivalent to Compose's `Button`).
370pub fn Button(
371    modifier: Modifier,
372    on_click: impl Fn() + 'static,
373    content: impl FnOnce() -> View,
374) -> View {
375    FilledButton(modifier, on_click, ButtonConfig::default(), content)
376}
377
378/// M3 Filled Button - prominent action button with primary color fill.
379pub fn FilledButton(
380    modifier: Modifier,
381    on_click: impl Fn() + 'static,
382    config: ButtonConfig,
383    content: impl FnOnce() -> View,
384) -> View {
385    let cc = config
386        .content_color
387        .unwrap_or_else(ButtonDefaults::content_color);
388    let bg = config
389        .container_color
390        .unwrap_or_else(ButtonDefaults::container_color);
391    let sc = config
392        .state_colors
393        .unwrap_or_else(ButtonDefaults::state_colors_default);
394    let se = config
395        .state_elevation
396        .unwrap_or_else(ButtonDefaults::state_elevation_default);
397    let pad = config.content_padding.unwrap_or(PaddingValues {
398        left: 24.0,
399        right: 24.0,
400        top: 0.0,
401        bottom: 0.0,
402    });
403    button_impl(
404        modifier.then(config.modifier),
405        on_click,
406        content,
407        cc,
408        Some(bg),
409        sc,
410        Some(se),
411        config.border,
412        pad.left,
413        pad.right,
414        config.height,
415        config.shape_radius,
416        config.enabled,
417    )
418}
419
420/// M3 Filled Tonal Button - uses secondary container colors.
421pub fn FilledTonalButton(
422    modifier: Modifier,
423    on_click: impl Fn() + 'static,
424    config: ButtonConfig,
425    content: impl FnOnce() -> View,
426) -> View {
427    let cc = config
428        .content_color
429        .unwrap_or_else(ButtonDefaults::tonal_content_color);
430    let bg = config
431        .container_color
432        .unwrap_or_else(ButtonDefaults::tonal_container_color);
433    let sc = config
434        .state_colors
435        .unwrap_or_else(ButtonDefaults::state_colors_default);
436    let se = config
437        .state_elevation
438        .unwrap_or_else(ButtonDefaults::state_elevation_default);
439    let pad = config.content_padding.unwrap_or(PaddingValues {
440        left: 24.0,
441        right: 24.0,
442        top: 0.0,
443        bottom: 0.0,
444    });
445    button_impl(
446        modifier.then(config.modifier),
447        on_click,
448        content,
449        cc,
450        Some(bg),
451        sc,
452        Some(se),
453        config.border,
454        pad.left,
455        pad.right,
456        config.height,
457        config.shape_radius,
458        config.enabled,
459    )
460}
461
462/// M3 Outlined Button - button with an outline border and no fill.
463pub fn OutlinedButton(
464    modifier: Modifier,
465    on_click: impl Fn() + 'static,
466    config: ButtonConfig,
467    content: impl FnOnce() -> View,
468) -> View {
469    let cc = config
470        .content_color
471        .unwrap_or_else(ButtonDefaults::outlined_content_color);
472    let sc = config
473        .state_colors
474        .unwrap_or_else(ButtonDefaults::state_colors_default);
475    let border = config
476        .border
477        .unwrap_or((1.0, ButtonDefaults::outlined_border_color(), 20.0));
478    let pad = config.content_padding.unwrap_or(PaddingValues {
479        left: 24.0,
480        right: 24.0,
481        top: 0.0,
482        bottom: 0.0,
483    });
484    button_impl(
485        modifier.then(config.modifier),
486        on_click,
487        content,
488        cc,
489        None,
490        sc,
491        None,
492        Some(border),
493        pad.left,
494        pad.right,
495        config.height,
496        config.shape_radius,
497        config.enabled,
498    )
499}
500
501/// M3 Text Button - a low-emphasis button.
502pub fn TextButton(
503    modifier: Modifier,
504    on_click: impl Fn() + 'static,
505    config: ButtonConfig,
506    content: impl FnOnce() -> View,
507) -> View {
508    let cc = config
509        .content_color
510        .unwrap_or_else(ButtonDefaults::text_content_color);
511    let sc = config
512        .state_colors
513        .unwrap_or_else(ButtonDefaults::state_colors_default);
514    let pad = config.content_padding.unwrap_or(PaddingValues {
515        left: 12.0,
516        right: 12.0,
517        top: 0.0,
518        bottom: 0.0,
519    });
520    button_impl(
521        modifier.then(config.modifier),
522        on_click,
523        content,
524        cc,
525        None,
526        sc,
527        None,
528        None,
529        pad.left,
530        pad.right,
531        config.height,
532        config.shape_radius,
533        config.enabled,
534    )
535}
536
537/// M3 Elevated Button - uses `surface_container_low` background with elevation.
538pub fn ElevatedButton(
539    modifier: Modifier,
540    on_click: impl Fn() + 'static,
541    config: ButtonConfig,
542    content: impl FnOnce() -> View,
543) -> View {
544    let cc = config
545        .content_color
546        .unwrap_or_else(ButtonDefaults::elevated_content_color);
547    let bg = config
548        .container_color
549        .unwrap_or_else(ButtonDefaults::elevated_container_color);
550    let sc = config
551        .state_colors
552        .unwrap_or_else(ButtonDefaults::state_colors_default);
553    let se = config
554        .state_elevation
555        .unwrap_or_else(ButtonDefaults::elevated_state_elevation);
556    let pad = config.content_padding.unwrap_or(PaddingValues {
557        left: 24.0,
558        right: 24.0,
559        top: 0.0,
560        bottom: 0.0,
561    });
562    button_impl(
563        modifier.then(config.modifier),
564        on_click,
565        content,
566        cc,
567        Some(bg),
568        sc,
569        Some(se),
570        config.border,
571        pad.left,
572        pad.right,
573        config.height,
574        config.shape_radius,
575        config.enabled,
576    )
577}
578
579/// Configuration for toggle button components.
580#[derive(Clone, Debug)]
581pub struct ToggleButtonConfig {
582    pub modifier: Modifier,
583    pub enabled: bool,
584    pub container_color: Option<Color>,
585    pub content_color: Option<Color>,
586    pub checked_container_color: Option<Color>,
587    pub checked_content_color: Option<Color>,
588    pub state_colors: Option<StateColors>,
589    pub state_elevation: Option<StateElevation>,
590    pub border: Option<(f32, Color, f32)>,
591    pub shape_radius: f32,
592    pub height: f32,
593}
594
595impl Default for ToggleButtonConfig {
596    fn default() -> Self {
597        Self {
598            modifier: Modifier::new(),
599            enabled: true,
600            container_color: None,
601            content_color: None,
602            checked_container_color: None,
603            checked_content_color: None,
604            state_colors: None,
605            state_elevation: None,
606            border: None,
607            shape_radius: ToggleButtonDefaults::SHAPE_RADIUS,
608            height: ToggleButtonDefaults::HEIGHT,
609        }
610    }
611}
612
613fn toggle_button_impl(
614    checked: bool,
615    on_checked_change: impl Fn(bool) + 'static,
616    content: impl FnOnce(bool) -> View,
617    content_color: Color,
618    container_color: Option<Color>,
619    checked_container_color: Option<Color>,
620    checked_content_color: Option<Color>,
621    state_colors: StateColors,
622    state_elevation: StateElevation,
623    border: Option<(f32, Color, f32)>,
624    pad_left: f32,
625    pad_right: f32,
626    height: f32,
627    shape_radius: f32,
628    enabled: bool,
629) -> View {
630    let th = theme();
631    let bg = if checked {
632        checked_container_color.unwrap_or(th.primary)
633    } else {
634        container_color.unwrap_or(Color::TRANSPARENT)
635    };
636    let fg = if checked {
637        checked_content_color.unwrap_or(th.on_primary)
638    } else {
639        content_color
640    };
641    let mut m = Modifier::new()
642        .height(height)
643        .padding_values(PaddingValues {
644            left: pad_left,
645            right: pad_right,
646            top: 0.0,
647            bottom: 0.0,
648        })
649        .background(bg)
650        .clip_rounded(shape_radius)
651        .state_colors(state_colors)
652        .state_elevation(state_elevation);
653    if let Some((w, c, r)) = border {
654        m = m.border(w, c, r);
655    }
656    if enabled {
657        m = m.clickable().on_pointer_down({
658            let cb = on_checked_change;
659            move |_| cb(!checked)
660        });
661    } else {
662        m = m.alpha(0.38);
663    }
664    with_content_color(fg, || Box(m).child(content(checked)))
665}
666
667/// M3 Toggle Button - a button that toggles between checked/unchecked states.
668pub fn ToggleButton(
669    checked: bool,
670    on_checked_change: impl Fn(bool) + 'static,
671    config: ToggleButtonConfig,
672    content: impl FnOnce(bool) -> View,
673) -> View {
674    let cc = config
675        .content_color
676        .unwrap_or_else(ToggleButtonDefaults::content_color);
677    let checked_cc = config
678        .checked_content_color
679        .unwrap_or_else(ToggleButtonDefaults::checked_content_color);
680    let checked_bg = config
681        .checked_container_color
682        .unwrap_or_else(ToggleButtonDefaults::checked_container_color);
683    let sc = config
684        .state_colors
685        .unwrap_or_else(ToggleButtonDefaults::state_colors_default);
686    let se = config
687        .state_elevation
688        .unwrap_or_else(ToggleButtonDefaults::state_elevation_default);
689    toggle_button_impl(
690        checked,
691        on_checked_change,
692        content,
693        cc,
694        None,
695        Some(checked_bg),
696        Some(checked_cc),
697        sc,
698        se,
699        config.border,
700        ToggleButtonDefaults::HORIZONTAL_PADDING,
701        ToggleButtonDefaults::HORIZONTAL_PADDING,
702        config.height,
703        config.shape_radius,
704        config.enabled,
705    )
706}
707
708/// M3 Tonal Toggle Button - uses secondary container colors.
709pub fn TonalToggleButton(
710    checked: bool,
711    on_checked_change: impl Fn(bool) + 'static,
712    config: ToggleButtonConfig,
713    content: impl FnOnce(bool) -> View,
714) -> View {
715    let cc = config
716        .content_color
717        .unwrap_or_else(ToggleButtonDefaults::tonal_content_color);
718    let checked_cc = config
719        .checked_content_color
720        .unwrap_or_else(ToggleButtonDefaults::tonal_checked_content_color);
721    let checked_bg = config
722        .checked_container_color
723        .unwrap_or_else(ToggleButtonDefaults::tonal_checked_container_color);
724    let sc = config
725        .state_colors
726        .unwrap_or_else(ToggleButtonDefaults::state_colors_default);
727    let se = config
728        .state_elevation
729        .unwrap_or_else(ToggleButtonDefaults::state_elevation_default);
730    toggle_button_impl(
731        checked,
732        on_checked_change,
733        content,
734        cc,
735        None,
736        Some(checked_bg),
737        Some(checked_cc),
738        sc,
739        se,
740        config.border,
741        ToggleButtonDefaults::HORIZONTAL_PADDING,
742        ToggleButtonDefaults::HORIZONTAL_PADDING,
743        config.height,
744        config.shape_radius,
745        config.enabled,
746    )
747}
748
749/// M3 Outlined Toggle Button - outlined button that toggles between states.
750pub fn OutlinedToggleButton(
751    checked: bool,
752    on_checked_change: impl Fn(bool) + 'static,
753    config: ToggleButtonConfig,
754    content: impl FnOnce(bool) -> View,
755) -> View {
756    let cc = config
757        .content_color
758        .unwrap_or_else(ToggleButtonDefaults::outlined_content_color);
759    let checked_cc = config
760        .checked_content_color
761        .unwrap_or_else(ToggleButtonDefaults::outlined_checked_content_color);
762    let checked_bg = config
763        .checked_container_color
764        .unwrap_or_else(ToggleButtonDefaults::outlined_checked_container_color);
765    let sc = config
766        .state_colors
767        .unwrap_or_else(ToggleButtonDefaults::state_colors_default);
768    let se = config
769        .state_elevation
770        .unwrap_or_else(ToggleButtonDefaults::state_elevation_default);
771    let border = if !checked {
772        Some(config.border.unwrap_or((
773            1.0,
774            ToggleButtonDefaults::outlined_border_color(),
775            config.shape_radius,
776        )))
777    } else {
778        config.border
779    };
780    toggle_button_impl(
781        checked,
782        on_checked_change,
783        content,
784        cc,
785        None,
786        Some(checked_bg),
787        Some(checked_cc),
788        sc,
789        se,
790        border,
791        ToggleButtonDefaults::HORIZONTAL_PADDING,
792        ToggleButtonDefaults::HORIZONTAL_PADDING,
793        config.height,
794        config.shape_radius,
795        config.enabled,
796    )
797}
798
799/// M3 Elevated Toggle Button - elevated button that toggles between states.
800pub fn ElevatedToggleButton(
801    checked: bool,
802    on_checked_change: impl Fn(bool) + 'static,
803    config: ToggleButtonConfig,
804    content: impl FnOnce(bool) -> View,
805) -> View {
806    let cc = config
807        .content_color
808        .unwrap_or_else(ToggleButtonDefaults::elevated_content_color);
809    let checked_cc = config
810        .checked_content_color
811        .unwrap_or_else(ToggleButtonDefaults::elevated_checked_content_color);
812    let checked_bg = config
813        .checked_container_color
814        .unwrap_or_else(ToggleButtonDefaults::elevated_checked_container_color);
815    let sc = config
816        .state_colors
817        .unwrap_or_else(ToggleButtonDefaults::state_colors_default);
818    let se = config
819        .state_elevation
820        .unwrap_or_else(ToggleButtonDefaults::elevated_state_elevation);
821    toggle_button_impl(
822        checked,
823        on_checked_change,
824        content,
825        cc,
826        None,
827        Some(checked_bg),
828        Some(checked_cc),
829        sc,
830        se,
831        config.border,
832        ToggleButtonDefaults::HORIZONTAL_PADDING,
833        ToggleButtonDefaults::HORIZONTAL_PADDING,
834        config.height,
835        config.shape_radius,
836        config.enabled,
837    )
838}
839
840/// Configuration for FAB components.
841#[derive(Clone, Debug)]
842pub struct FABConfig {
843    pub modifier: Modifier,
844    pub container_color: Color,
845    pub content_color: Color,
846    pub state_elevation: StateElevation,
847    pub shape_radius: f32,
848    pub size: f32,
849}
850
851impl Default for FABConfig {
852    fn default() -> Self {
853        Self {
854            modifier: Modifier::new(),
855            container_color: FABDefaults::container_color(),
856            content_color: FABDefaults::content_color(),
857            state_elevation: FABDefaults::state_elevation(),
858            shape_radius: FABDefaults::SHAPE_RADIUS,
859            size: FABDefaults::SIZE,
860        }
861    }
862}
863
864fn fab_impl(
865    icon: View,
866    on_click: impl Fn() + 'static,
867    size: f32,
868    shape_r: f32,
869    config: FABConfig,
870) -> View {
871    let th = theme();
872    Box(Modifier::new()
873        .size(size, size)
874        .background(config.container_color)
875        .state_colors(StateColors {
876            default: Color::TRANSPARENT,
877            hovered: config.content_color.with_alpha_f32(0.08),
878            pressed: config.content_color.with_alpha_f32(0.12),
879            disabled: th.on_surface.with_alpha_f32(0.12),
880        })
881        .state_elevation(config.state_elevation)
882        .clip_rounded(shape_r)
883        .align_items(AlignItems::Center)
884        .justify_content(JustifyContent::Center)
885        .clickable()
886        .on_pointer_down(move |_| on_click())
887        .then(config.modifier))
888    .child(icon)
889}
890
891/// M3 Floating Action Button (regular, 56dp).
892pub fn FAB(icon: View, on_click: impl Fn() + 'static, config: FABConfig) -> View {
893    fab_impl(
894        icon,
895        on_click,
896        FABDefaults::SIZE,
897        FABDefaults::SHAPE_RADIUS,
898        config,
899    )
900}
901
902/// M3 Large FAB (96dp).
903pub fn LargeFAB(icon: View, on_click: impl Fn() + 'static, config: FABConfig) -> View {
904    fab_impl(
905        icon,
906        on_click,
907        FABDefaults::LARGE_SIZE,
908        FABDefaults::LARGE_SHAPE_RADIUS,
909        config,
910    )
911}
912
913/// M3 Extended FAB - FAB with icon + label.
914pub fn ExtendedFAB(
915    icon: Option<View>,
916    label: impl Into<String>,
917    on_click: impl Fn() + 'static,
918    config: FABConfig,
919) -> View {
920    let th = theme();
921    let has_icon = icon.is_some();
922    Row(Modifier::new()
923        .height(56.0)
924        .min_width(80.0)
925        .background(config.container_color)
926        .state_colors(StateColors {
927            default: Color::TRANSPARENT,
928            hovered: config.content_color.with_alpha_f32(0.08),
929            pressed: config.content_color.with_alpha_f32(0.12),
930            disabled: theme().on_surface.with_alpha_f32(0.12),
931        })
932        .state_elevation(config.state_elevation)
933        .clip_rounded(16.0)
934        .padding_values(PaddingValues {
935            left: 16.0,
936            right: 20.0,
937            top: 0.0,
938            bottom: 0.0,
939        })
940        .align_items(AlignItems::Center)
941        .clickable()
942        .on_pointer_down(move |_| on_click())
943        .then(config.modifier))
944    .child((
945        icon.unwrap_or(Box(Modifier::new())),
946        Box(Modifier::new()
947            .width(if has_icon { 12.0 } else { 0.0 })
948            .fill_max_height()),
949        Text(label)
950            .color(config.content_color)
951            .size(th.typography.label_large)
952            .single_line(),
953    ))
954}
955
956/// Configuration for divider components.
957#[derive(Clone, Debug)]
958pub struct DividerConfig {
959    pub modifier: Modifier,
960    pub thickness: f32,
961    pub color: Color,
962}
963
964impl Default for DividerConfig {
965    fn default() -> Self {
966        Self {
967            modifier: Modifier::new(),
968            thickness: DividerDefaults::THICKNESS,
969            color: DividerDefaults::color(),
970        }
971    }
972}
973
974/// M3 Horizontal Divider - a thin 1dp line.
975pub fn Divider(config: DividerConfig) -> View {
976    Box(Modifier::new()
977        .min_width(200.0)
978        .height(config.thickness)
979        .background(config.color)
980        .then(config.modifier))
981}
982
983/// M3 Vertical Divider - a thin 1dp vertical line.
984pub fn VerticalDivider(config: DividerConfig) -> View {
985    Box(Modifier::new()
986        .width(config.thickness)
987        .fill_max_height()
988        .background(config.color)
989        .then(config.modifier))
990}
991
992/// Configuration for [`Badge`].
993#[derive(Clone, Debug)]
994pub struct BadgeConfig {
995    pub modifier: Modifier,
996    pub color: Color,
997    pub label_color: Color,
998}
999
1000impl Default for BadgeConfig {
1001    fn default() -> Self {
1002        Self {
1003            modifier: Modifier::new(),
1004            color: BadgeDefaults::color(),
1005            label_color: BadgeDefaults::label_color(),
1006        }
1007    }
1008}
1009
1010/// M3 Badge - a small notification indicator. If `label` is `None`, shows a
1011/// small 6dp dot; otherwise shows the label text inside a 16dp pill.
1012pub fn Badge(label: Option<impl Into<String>>, config: BadgeConfig) -> View {
1013    let th = theme();
1014    match label {
1015        None => Box(Modifier::new()
1016            .size(BadgeDefaults::DOT_SIZE, BadgeDefaults::DOT_SIZE)
1017            .background(config.color)
1018            .clip_rounded(BadgeDefaults::DOT_SIZE * 0.5)
1019            .then(config.modifier)),
1020        Some(text) => {
1021            let text = text.into();
1022            Box(Modifier::new()
1023                .min_width(BadgeDefaults::LABEL_MIN_WIDTH)
1024                .height(BadgeDefaults::LABEL_HEIGHT)
1025                .background(config.color)
1026                .clip_rounded(BadgeDefaults::LABEL_HEIGHT * 0.5)
1027                .padding_values(PaddingValues {
1028                    left: 4.0,
1029                    right: 4.0,
1030                    top: 0.0,
1031                    bottom: 0.0,
1032                })
1033                .align_items(AlignItems::Center)
1034                .justify_content(JustifyContent::Center)
1035                .then(config.modifier))
1036            .child(
1037                Text(text)
1038                    .color(config.label_color)
1039                    .size(th.typography.label_small)
1040                    .single_line(),
1041            )
1042        }
1043    }
1044}
1045
1046/// Configuration for [`ListItem`].
1047#[derive(Clone, Debug)]
1048pub struct ListItemConfig {
1049    pub modifier: Modifier,
1050    pub headline_color: Color,
1051    pub supporting_color: Color,
1052    pub horizontal_padding: f32,
1053    pub trailing_padding: f32,
1054    pub one_line_height: f32,
1055    pub two_line_height: f32,
1056}
1057
1058impl Default for ListItemConfig {
1059    fn default() -> Self {
1060        Self {
1061            modifier: Modifier::new(),
1062            headline_color: ListItemDefaults::headline_color(),
1063            supporting_color: ListItemDefaults::supporting_color(),
1064            horizontal_padding: ListItemDefaults::HORIZONTAL_PADDING,
1065            trailing_padding: ListItemDefaults::TRAILING_PADDING,
1066            one_line_height: ListItemDefaults::ONE_LINE_HEIGHT,
1067            two_line_height: ListItemDefaults::TWO_LINE_HEIGHT,
1068        }
1069    }
1070}
1071
1072/// M3 List Item - a single row in a list with optional leading/trailing content.
1073pub fn ListItem(
1074    headline: impl Into<String>,
1075    supporting_text: Option<String>,
1076    leading: Option<View>,
1077    trailing: Option<View>,
1078    on_click: Option<Rc<dyn Fn()>>,
1079    config: ListItemConfig,
1080) -> View {
1081    let th = theme();
1082    let mut modifier = Modifier::new()
1083        .min_width(200.0)
1084        .min_height(if supporting_text.is_some() {
1085            config.two_line_height
1086        } else {
1087            config.one_line_height
1088        })
1089        .state_colors(StateColors {
1090            default: Color::TRANSPARENT,
1091            hovered: th.on_surface.with_alpha_f32(0.08),
1092            pressed: th.on_surface.with_alpha_f32(0.12),
1093            disabled: Color::TRANSPARENT,
1094        })
1095        .padding_values(PaddingValues {
1096            left: config.horizontal_padding,
1097            right: config.trailing_padding,
1098            top: 8.0,
1099            bottom: 8.0,
1100        })
1101        .align_items(AlignItems::Center)
1102        .then(config.modifier);
1103
1104    if let Some(cb) = on_click {
1105        modifier = modifier.clickable().on_pointer_down(move |_| cb());
1106    }
1107
1108    Row(modifier).child((
1109        leading
1110            .map(|v| {
1111                Box(Modifier::new().padding_values(PaddingValues {
1112                    left: 0.0,
1113                    right: 16.0,
1114                    top: 0.0,
1115                    bottom: 0.0,
1116                }))
1117                .child(v)
1118            })
1119            .unwrap_or(Box(Modifier::new())),
1120        Column(
1121            Modifier::new()
1122                .flex_grow(1.0)
1123                .justify_content(JustifyContent::Center),
1124        )
1125        .child((
1126            Text(headline)
1127                .color(config.headline_color)
1128                .size(th.typography.body_large)
1129                .single_line(),
1130            supporting_text
1131                .map(|st| {
1132                    Text(st)
1133                        .color(config.supporting_color)
1134                        .size(th.typography.body_medium)
1135                        .max_lines(2)
1136                        .overflow_ellipsize()
1137                })
1138                .unwrap_or(Box(Modifier::new())),
1139        )),
1140        trailing
1141            .map(|v| {
1142                Box(Modifier::new().padding_values(PaddingValues {
1143                    left: 16.0,
1144                    right: 0.0,
1145                    top: 0.0,
1146                    bottom: 0.0,
1147                }))
1148                .child(v)
1149            })
1150            .unwrap_or(Box(Modifier::new())),
1151    ))
1152}
1153
1154/// A single tab definition for use with `TabRow`.
1155pub struct Tab {
1156    pub label: String,
1157    pub icon: Option<View>,
1158    pub on_click: Rc<dyn Fn()>,
1159}
1160
1161/// Configuration for [`TabRow`].
1162#[derive(Clone, Debug)]
1163pub struct TabRowConfig {
1164    pub modifier: Modifier,
1165    pub container_color: Color,
1166    pub selected_content_color: Color,
1167    pub unselected_content_color: Color,
1168    pub indicator_color: Color,
1169    pub height: f32,
1170    pub indicator_height: f32,
1171}
1172
1173impl Default for TabRowConfig {
1174    fn default() -> Self {
1175        Self {
1176            modifier: Modifier::new(),
1177            container_color: TabDefaults::container_color(),
1178            selected_content_color: TabDefaults::selected_content_color(),
1179            unselected_content_color: TabDefaults::unselected_content_color(),
1180            indicator_color: TabDefaults::indicator_color(),
1181            height: TabDefaults::HEIGHT,
1182            indicator_height: TabDefaults::INDICATOR_HEIGHT,
1183        }
1184    }
1185}
1186
1187static TABROW_COUNTER: AtomicU64 = AtomicU64::new(0);
1188
1189/// M3 Tab Row - a horizontal row of tabs with an active indicator.
1190/// Text colors and indicator height animate with 150ms FastOutSlowIn.
1191pub fn TabRow(selected_index: usize, tabs: Vec<Tab>, config: TabRowConfig) -> View {
1192    let th = theme();
1193    let id = remember(|| TABROW_COUNTER.fetch_add(1, Ordering::Relaxed));
1194    let spec = th.motion.color;
1195    Column(Modifier::new().fill_max_width())
1196        .child((
1197            Row(Modifier::new()
1198                .fill_max_width()
1199                .height(config.height)
1200                .background(config.container_color))
1201            .child(
1202                tabs.into_iter()
1203                    .enumerate()
1204                    .map(|(i, tab)| {
1205                        let selected = i == selected_index;
1206                        let color = animate_color(
1207                            format!("tab_clr_{}_{}", id, i),
1208                            if selected {
1209                                config.selected_content_color
1210                            } else {
1211                                config.unselected_content_color
1212                            },
1213                            spec,
1214                        );
1215                        let indicator_h = animate_f32(
1216                            format!("tab_ind_{}_{}", id, i),
1217                            if selected {
1218                                config.indicator_height
1219                            } else {
1220                                0.0
1221                            },
1222                            spec,
1223                        );
1224                        let cb = tab.on_click.clone();
1225
1226                        Column(
1227                            Modifier::new()
1228                                .flex_grow(1.0)
1229                                .fill_max_height()
1230                                .align_items(AlignItems::Center)
1231                                .justify_content(JustifyContent::Center)
1232                                .state_colors(StateColors {
1233                                    default: Color::TRANSPARENT,
1234                                    hovered: th.on_surface.with_alpha_f32(0.08),
1235                                    pressed: th.on_surface.with_alpha_f32(0.12),
1236                                    disabled: Color::TRANSPARENT,
1237                                })
1238                                .clickable()
1239                                .on_pointer_down(move |_| cb()),
1240                        )
1241                        .child((
1242                            tab.icon.unwrap_or(Box(Modifier::new())),
1243                            Text(tab.label)
1244                                .color(color)
1245                                .size(th.typography.title_small)
1246                                .single_line(),
1247                            Box(Modifier::new()
1248                                .min_width(200.0)
1249                                .height(indicator_h)
1250                                .background(config.indicator_color)
1251                                .clip_rounded(TabDefaults::INDICATOR_CORNER)),
1252                        ))
1253                    })
1254                    .collect::<Vec<_>>(),
1255            ),
1256            Box(Modifier::new()
1257                .min_width(200.0)
1258                .height(1.0)
1259                .background(th.outline_variant)),
1260        ))
1261        .modifier(config.modifier)
1262}
1263
1264/// A single segment definition for `SegmentedButton`.
1265pub struct Segment {
1266    pub label: String,
1267    pub icon: Option<View>,
1268    pub on_click: Rc<dyn Fn()>,
1269}
1270
1271/// Configuration for [`SegmentedButton`].
1272#[derive(Clone, Debug)]
1273pub struct SegmentedButtonConfig {
1274    pub modifier: Modifier,
1275    pub border_color: Color,
1276    pub selected_container_color: Color,
1277    pub selected_content_color: Color,
1278    pub unselected_content_color: Color,
1279    pub height: f32,
1280    pub shape_radius: f32,
1281}
1282
1283impl Default for SegmentedButtonConfig {
1284    fn default() -> Self {
1285        Self {
1286            modifier: Modifier::new(),
1287            border_color: SegmentedButtonDefaults::border_color(),
1288            selected_container_color: SegmentedButtonDefaults::selected_container_color(),
1289            selected_content_color: SegmentedButtonDefaults::selected_content_color(),
1290            unselected_content_color: SegmentedButtonDefaults::unselected_content_color(),
1291            height: SegmentedButtonDefaults::HEIGHT,
1292            shape_radius: SegmentedButtonDefaults::SHAPE_RADIUS,
1293        }
1294    }
1295}
1296
1297static SEGBUTTON_COUNTER: AtomicU64 = AtomicU64::new(0);
1298
1299/// M3 Segmented Button - a row of toggle segments. `selected` contains the
1300/// indices of selected segments (single-select: pass a single-element set).
1301pub fn SegmentedButton(
1302    selected: &[usize],
1303    segments: Vec<Segment>,
1304    config: SegmentedButtonConfig,
1305) -> View {
1306    let th = theme();
1307    let count = segments.len();
1308    let id = remember(|| SEGBUTTON_COUNTER.fetch_add(1, Ordering::Relaxed));
1309    let spec = th.motion.color;
1310
1311    Row(Modifier::new()
1312        .height(config.height)
1313        .border(1.0, config.border_color, config.shape_radius)
1314        .clip_rounded(config.shape_radius)
1315        .then(config.modifier))
1316    .child(
1317        segments
1318            .into_iter()
1319            .enumerate()
1320            .map(|(i, seg)| {
1321                let is_selected = selected.contains(&i);
1322
1323                let bg = animate_color(
1324                    format!("sb_bg_{}_{}", id, i),
1325                    if is_selected {
1326                        config.selected_container_color
1327                    } else {
1328                        Color::TRANSPARENT
1329                    },
1330                    spec,
1331                );
1332                let fg = animate_color(
1333                    format!("sb_fg_{}_{}", id, i),
1334                    if is_selected {
1335                        config.selected_content_color
1336                    } else {
1337                        config.unselected_content_color
1338                    },
1339                    spec,
1340                );
1341
1342                let cb = seg.on_click.clone();
1343
1344                let mut modifier = Modifier::new()
1345                    .flex_grow(1.0)
1346                    .fill_max_height()
1347                    .background(bg)
1348                    .align_items(AlignItems::Center)
1349                    .justify_content(JustifyContent::Center)
1350                    .padding_values(PaddingValues {
1351                        left: 12.0,
1352                        right: 12.0,
1353                        top: 0.0,
1354                        bottom: 0.0,
1355                    })
1356                    .clickable()
1357                    .on_pointer_down(move |_| cb());
1358
1359                if i < count - 1 {
1360                    modifier = modifier.border(1.0, th.outline, 0.0);
1361                }
1362
1363                Row(modifier).child((
1364                    seg.icon.unwrap_or(Box(Modifier::new())),
1365                    Text(seg.label)
1366                        .color(fg)
1367                        .size(th.typography.label_large)
1368                        .single_line(),
1369                ))
1370            })
1371            .collect::<Vec<_>>(),
1372    )
1373}
1374
1375/// Configuration for [`CircularProgressIndicator`].
1376#[derive(Clone, Debug)]
1377pub struct CircularProgressIndicatorConfig {
1378    pub color: Color,
1379    pub track_color: Color,
1380}
1381
1382impl Default for CircularProgressIndicatorConfig {
1383    fn default() -> Self {
1384        Self {
1385            color: ProgressIndicatorDefaults::circular_color(),
1386            track_color: ProgressIndicatorDefaults::circular_track_color(),
1387        }
1388    }
1389}
1390
1391pub fn CircularProgressIndicator(
1392    value: Option<f32>,
1393    config: CircularProgressIndicatorConfig,
1394) -> View {
1395    let sz = dp_to_px(ProgressIndicatorDefaults::CIRCULAR_INDICATOR_SIZE);
1396    let stroke = dp_to_px(ProgressIndicatorDefaults::CIRCULAR_STROKE_WIDTH);
1397    let val = value.unwrap_or(0.0).clamp(0.0, 1.0);
1398
1399    Box(Modifier::new()
1400        .size(sz, sz)
1401        .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
1402            let mul_c = |c: Color| {
1403                Color(
1404                    c.0,
1405                    c.1,
1406                    c.2,
1407                    ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
1408                )
1409            };
1410            let cx = rect.x + rect.w * 0.5;
1411            let cy = rect.y + rect.h * 0.5;
1412            let r = (rect.w.min(rect.h)) * 0.5 - stroke * 0.5;
1413            let circle = Rect {
1414                x: cx - r,
1415                y: cy - r,
1416                w: r * 2.0,
1417                h: r * 2.0,
1418            };
1419
1420            // Track ring
1421            scene.nodes.push(SceneNode::EllipseBorder {
1422                rect: circle,
1423                color: mul_c(config.track_color),
1424                width: stroke,
1425            });
1426
1427            // Indicator: bottom-up fill approximation inside circle
1428            if val > 0.0 {
1429                let fill_h = r * 2.0 * val;
1430                scene.nodes.push(SceneNode::PushClip {
1431                    rect: Rect {
1432                        x: cx - r,
1433                        y: cy + r - fill_h,
1434                        w: r * 2.0,
1435                        h: fill_h,
1436                    },
1437                    radius: 0.0,
1438                });
1439                scene.nodes.push(SceneNode::EllipseBorder {
1440                    rect: circle,
1441                    color: mul_c(config.color),
1442                    width: stroke,
1443                });
1444                scene.nodes.push(SceneNode::PopClip);
1445            }
1446        }))
1447    .semantics(Semantics {
1448        role: Role::ProgressBar,
1449        label: None,
1450        focused: false,
1451        enabled: true,
1452    })
1453}
1454
1455/// Configuration for [`LinearProgressIndicator`].
1456#[derive(Clone, Debug)]
1457pub struct LinearProgressIndicatorConfig {
1458    pub color: Color,
1459    pub track_color: Color,
1460    /// Gap between indicator and track, in dp.
1461    pub gap_size: f32,
1462    /// Diameter of the stop indicator dot, in dp.
1463    pub stop_size: f32,
1464}
1465
1466impl Default for LinearProgressIndicatorConfig {
1467    fn default() -> Self {
1468        Self {
1469            color: ProgressIndicatorDefaults::linear_color(),
1470            track_color: ProgressIndicatorDefaults::linear_track_color(),
1471            gap_size: ProgressIndicatorDefaults::LINEAR_INDICATOR_GAP_SIZE,
1472            stop_size: ProgressIndicatorDefaults::LINEAR_TRACK_STOP_SIZE,
1473        }
1474    }
1475}
1476
1477/// M3 Linear Progress Indicator.
1478///
1479/// Pass `LinearProgressIndicatorConfig::default()` for standard M3 appearance,
1480/// or override individual fields via struct-update syntax.
1481pub fn LinearProgressIndicator(value: Option<f32>, config: LinearProgressIndicatorConfig) -> View {
1482    Box(Modifier::new()
1483        .fill_max_width()
1484        .height(ProgressIndicatorDefaults::LINEAR_INDICATOR_HEIGHT)
1485        .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
1486            let mul_c = |c: Color| {
1487                Color(
1488                    c.0,
1489                    c.1,
1490                    c.2,
1491                    ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
1492                )
1493            };
1494            let track_h = rect.h;
1495            let corner = track_h * 0.5;
1496            let gap = dp_to_px(config.gap_size);
1497            let dot_r = dp_to_px(config.stop_size) * 0.5;
1498            let cy = rect.y + rect.h * 0.5;
1499            let t = value.unwrap_or(0.0).clamp(0.0, 1.0);
1500
1501            // Indicator (active portion from left)
1502            if t > 0.0 {
1503                let ind_w = t * rect.w;
1504                scene.nodes.push(SceneNode::Rect {
1505                    rect: Rect {
1506                        x: rect.x,
1507                        y: cy - corner,
1508                        w: ind_w,
1509                        h: track_h,
1510                    },
1511                    brush: Brush::Solid(mul_c(config.color)),
1512                    radius: corner,
1513                });
1514            }
1515
1516            // Track (inactive portion after gap)
1517            let track_start = rect.x + t * rect.w + gap;
1518            let track_w = (rect.x + rect.w - track_start).max(0.0);
1519            if t < 1.0 && track_w > 0.0 {
1520                scene.nodes.push(SceneNode::Rect {
1521                    rect: Rect {
1522                        x: track_start,
1523                        y: cy - corner,
1524                        w: track_w,
1525                        h: track_h,
1526                    },
1527                    brush: Brush::Solid(mul_c(config.track_color)),
1528                    radius: corner,
1529                });
1530            }
1531
1532            // Stop indicator at right end (4dp circle, Primary)
1533            if t < 1.0 {
1534                let sx = rect.x + rect.w - dot_r;
1535                scene.nodes.push(SceneNode::Ellipse {
1536                    rect: Rect {
1537                        x: sx - dot_r,
1538                        y: cy - dot_r,
1539                        w: dot_r * 2.0,
1540                        h: dot_r * 2.0,
1541                    },
1542                    brush: Brush::Solid(mul_c(config.color)),
1543                });
1544            }
1545        }))
1546    .semantics(Semantics {
1547        role: Role::ProgressBar,
1548        label: None,
1549        focused: false,
1550        enabled: true,
1551    })
1552}
1553
1554/// Configuration for an `OutlinedTextField`.
1555#[derive(Clone)]
1556pub struct OutlinedTextFieldConfig {
1557    /// Floating label shown above the input when the field has text or is focused.
1558    /// When set, this acts as the visual placeholder (the TextField's own placeholder
1559    /// is suppressed). When the label floats, it animates to the top border.
1560    pub label: Option<String>,
1561    /// Placeholder text shown inside the TextField when empty and unfocused.
1562    /// Only shown when `label` is `None`; when a label is present the label
1563    /// itself serves as the visual placeholder.
1564    pub placeholder: Option<String>,
1565    /// Icon displayed at the start of the input.
1566    pub leading_icon: Option<View>,
1567    /// Icon displayed at the end of the input.
1568    pub trailing_icon: Option<View>,
1569    /// If true, Enter submits; if false, Enter inserts a newline.
1570    pub single_line: bool,
1571    /// If true, border and label color switch to error color.
1572    pub is_error: bool,
1573    /// If false, input is visually disabled and `on_value_change` won't fire.
1574    pub enabled: bool,
1575    /// Called when the user presses Enter on a single-line field.
1576    pub on_submit: Option<Rc<dyn Fn(String)>>,
1577}
1578
1579impl Default for OutlinedTextFieldConfig {
1580    fn default() -> Self {
1581        Self {
1582            label: None,
1583            placeholder: None,
1584            leading_icon: None,
1585            trailing_icon: None,
1586            single_line: true,
1587            is_error: false,
1588            enabled: true,
1589            on_submit: None,
1590        }
1591    }
1592}
1593
1594/// M3 Outlined Text Field with floating label, leading/trailing icons, and error state.
1595///
1596/// The label floats up when `value` is non-empty or when the field is focused.
1597/// Note: focus-based floating is approximated via animated `float_t` - the label
1598/// begins floating once `on_value_change` fires (i.e. when the user types).
1599/// For strict focus-on-tap floating, pair with an external focus signal.
1600///
1601/// # Example
1602/// ```ignore
1603/// let text = remember(|| signal(String::new()));
1604/// OutlinedTextField(
1605///     Modifier::new().fill_max_width().padding(16.0),
1606///     text.get(),
1607///     { let t = text.clone(); move |v| t.set(v) },
1608///     OutlinedTextFieldConfig {
1609///         label: Some("Email".into()),
1610///         placeholder: Some("user@example.com".into()),
1611///         ..Default::default()
1612///     },
1613/// );
1614/// ```
1615pub fn OutlinedTextField(
1616    modifier: Modifier,
1617    value: String,
1618    on_value_change: impl Fn(String) + 'static,
1619    config: OutlinedTextFieldConfig,
1620) -> View {
1621    let th = theme();
1622    let label_str: Option<Rc<str>> = config.label.map(Rc::from);
1623    let has_label = label_str.is_some();
1624
1625    // Unique animation key per label to avoid conflicts when multiple fields exist
1626    let anim_key = match &label_str {
1627        Some(l) => format!("otf_{}", &l[..l.len().min(32)]),
1628        None => "otf_nolabel".into(),
1629    };
1630
1631    // Persistent focus tracker - set by layout/paint when this field is focused,
1632    // read here on the next frame. This gives a one-frame delay on tap-to-float,
1633    // which is negligible at 60fps.
1634    let focus_tracker: Rc<Cell<bool>> =
1635        remember_with_key(format!("otf_focus_{}", anim_key), || Cell::new(false));
1636    let is_focused = focus_tracker.get();
1637    let should_float = !value.is_empty() || is_focused;
1638
1639    let float_t = animate_f32(
1640        anim_key.clone(),
1641        if should_float { 1.0 } else { 0.0 },
1642        th.motion.color,
1643    );
1644
1645    // Border color: error > focused (float) > default
1646    let border_color = if config.is_error {
1647        th.error
1648    } else if float_t > 0.5 {
1649        th.primary
1650    } else {
1651        th.outline
1652    };
1653
1654    // Label color: error > focused > default
1655    let label_color = if config.is_error {
1656        th.error
1657    } else if float_t > 0.5 {
1658        th.primary
1659    } else {
1660        th.on_surface_variant
1661    };
1662
1663    // Label font size: 16dp at rest (placeholder position) → 12dp when floating
1664    let label_size = 16.0 - 4.0 * float_t;
1665
1666    // Label Y offset: 16dp (same line as text) → -4dp (overlapping top border)
1667    let label_y = 16.0 - 20.0 * float_t;
1668
1669    // The TextField inside uses no placeholder when a label is present -
1670    // the label itself serves as the visual placeholder.
1671    let tf_placeholder = if has_label {
1672        String::new()
1673    } else {
1674        config.placeholder.unwrap_or_default()
1675    };
1676
1677    Box(modifier
1678        .clip_rounded(th.shapes.small)
1679        .border(1.0, border_color, th.shapes.small)
1680        .background(th.surface))
1681    .child(
1682        Stack(Modifier::new().fill_max_size()).child((
1683            // Input row - always at the same position, with room at the top
1684            // for the floating label to overlap.
1685            Row(Modifier::new()
1686                .fill_max_size()
1687                .padding_values(PaddingValues {
1688                    left: 16.0,
1689                    right: 16.0,
1690                    top: 16.0,
1691                    bottom: 8.0,
1692                })
1693                .align_items(AlignItems::Center))
1694            .child((
1695                config.leading_icon.unwrap_or(Box(Modifier::new())),
1696                View::new(0, ViewKind::Box)
1697                    .modifier(
1698                        Modifier::new()
1699                            .flex_grow(1.0)
1700                            .padding_values(PaddingValues {
1701                                left: 8.0,
1702                                right: 8.0,
1703                                top: 0.0,
1704                                bottom: 0.0,
1705                            })
1706                            .text_input(TextInputConfig {
1707                                hint: tf_placeholder,
1708                                multiline: false,
1709                                on_change: Some(Rc::new(on_value_change) as _),
1710                                on_submit: config.on_submit.clone().map(|f| {
1711                                    let f = f.clone();
1712                                    Rc::new(move |s| f(s)) as Rc<dyn Fn(String)>
1713                                }),
1714                                focus_tracker: Some(focus_tracker.clone()),
1715                                value: value.clone(),
1716                                visual_transformation: None,
1717                                keyboard_type: None,
1718                                ime_action: None,
1719                            }),
1720                    )
1721                    .semantics(Semantics {
1722                        role: Role::TextField,
1723                        label: None,
1724                        focused: false,
1725                        enabled: true,
1726                    }),
1727                config.trailing_icon.unwrap_or(Box(Modifier::new())),
1728            )),
1729            // Floating label - absolutely positioned, animates between text-line
1730            // and top-border positions as the field gains content / focus.
1731            // A surface-colored background box hides the border stroke behind the label.
1732            if let Some(lbl) = label_str {
1733                Box(Modifier::new()
1734                    .min_width(200.0)
1735                    .padding_values(PaddingValues {
1736                        left: 20.0,
1737                        right: 20.0,
1738                        top: 0.0,
1739                        bottom: 0.0,
1740                    })
1741                    .absolute()
1742                    .offset(Some(0.0), Some(label_y), None, None))
1743                .child(
1744                    Box(Modifier::new()
1745                        .background(th.surface)
1746                        .padding_values(PaddingValues {
1747                            left: 4.0,
1748                            right: 4.0,
1749                            top: 2.0,
1750                            bottom: 2.0,
1751                        }))
1752                    .child(
1753                        Text(lbl.as_ref().to_string())
1754                            .color(label_color)
1755                            .size(label_size),
1756                    ),
1757                )
1758            } else {
1759                Box(Modifier::new())
1760            },
1761        )),
1762    )
1763}
1764
1765/// Configuration for [`Checkbox`].
1766#[derive(Clone, Debug)]
1767pub struct CheckboxConfig {
1768    pub modifier: Modifier,
1769    pub checked_color: Color,
1770    pub unchecked_color: Color,
1771    pub checkmark_color: Color,
1772}
1773
1774impl Default for CheckboxConfig {
1775    fn default() -> Self {
1776        Self {
1777            modifier: Modifier::new(),
1778            checked_color: CheckboxDefaults::checked_color(),
1779            unchecked_color: CheckboxDefaults::unchecked_color(),
1780            checkmark_color: CheckboxDefaults::checkmark_color(),
1781        }
1782    }
1783}
1784
1785/// M3 Checkbox.
1786/// Renders a 40dp touch-target with an 18dp check box inside.
1787/// Fill, border, and check mark animate with 100ms FastOutSlowIn.
1788static CHECKBOX_COUNTER: AtomicU64 = AtomicU64::new(0);
1789pub fn Checkbox(checked: bool, on_change: impl Fn(bool) + 'static, config: CheckboxConfig) -> View {
1790    let th = theme();
1791    let sz = CheckboxDefaults::BOX_SIZE;
1792
1793    let id = remember(|| CHECKBOX_COUNTER.fetch_add(1, Ordering::Relaxed));
1794    let spec = th.motion.color_fast;
1795
1796    let fill = animate_color(
1797        format!("cb_fill_{}", id),
1798        if checked {
1799            config.checked_color
1800        } else {
1801            Color::TRANSPARENT
1802        },
1803        spec,
1804    );
1805    let bd_w = animate_f32(
1806        format!("cb_bw_{}", id),
1807        if checked {
1808            0.0
1809        } else {
1810            CheckboxDefaults::STROKE_WIDTH
1811        },
1812        spec,
1813    );
1814    let bd = animate_color(
1815        format!("cb_bd_{}", id),
1816        if checked {
1817            Color::TRANSPARENT
1818        } else {
1819            config.unchecked_color
1820        },
1821        spec,
1822    );
1823    let check_alpha = animate_f32(
1824        format!("cb_ca_{}", id),
1825        if checked { 1.0 } else { 0.0 },
1826        spec,
1827    );
1828
1829    Box(Modifier::new()
1830        .width(CheckboxDefaults::TOUCH_TARGET_SIZE)
1831        .height(CheckboxDefaults::TOUCH_TARGET_SIZE)
1832        .padding(0.0)
1833        .clip_rounded(20.0)
1834        .background(Color::TRANSPARENT)
1835        .clickable()
1836        .align_items(AlignItems::Center)
1837        .justify_content(JustifyContent::Center)
1838        .on_pointer_down(move |_| on_change(!checked))
1839        .then(config.modifier))
1840    .child(
1841        Box(Modifier::new()
1842            .size(sz, sz)
1843            .background(fill)
1844            .border(bd_w, bd, CheckboxDefaults::CORNER_RADIUS)
1845            .clip_rounded(CheckboxDefaults::CORNER_RADIUS)
1846            .align_items(AlignItems::Center)
1847            .justify_content(JustifyContent::Center))
1848        .child(if check_alpha > 0.01 {
1849            Box(Modifier::new().alpha(check_alpha)).child(
1850                Icon(Symbol::new("done", '\u{E876}'))
1851                    .color(config.checkmark_color)
1852                    .size(CheckboxDefaults::CHECK_ICON_SIZE),
1853            )
1854        } else {
1855            Box(Modifier::new())
1856        }),
1857    )
1858}
1859
1860/// Configuration for [`RadioButton`].
1861#[derive(Clone, Debug)]
1862pub struct RadioButtonConfig {
1863    pub modifier: Modifier,
1864    pub selected_color: Color,
1865    pub unselected_color: Color,
1866}
1867
1868impl Default for RadioButtonConfig {
1869    fn default() -> Self {
1870        Self {
1871            modifier: Modifier::new(),
1872            selected_color: RadioButtonDefaults::selected_color(),
1873            unselected_color: RadioButtonDefaults::unselected_color(),
1874        }
1875    }
1876}
1877
1878/// M3 RadioButton.
1879/// Renders a 40dp touch-target with a 20dp outer circle + inner dot.
1880/// Ring color animates with 100ms FastOutSlowIn; dot size animates with spring.
1881static RADIO_COUNTER: AtomicU64 = AtomicU64::new(0);
1882pub fn RadioButton(
1883    selected: bool,
1884    on_select: impl Fn() + 'static,
1885    config: RadioButtonConfig,
1886) -> View {
1887    let th = theme();
1888    let d = RadioButtonDefaults::OUTER_RADIUS * 2.0;
1889
1890    let id = remember(|| RADIO_COUNTER.fetch_add(1, Ordering::Relaxed));
1891    let color_spec = th.motion.color_fast;
1892    let spring = th.motion.spring;
1893
1894    let ring_col = animate_color(
1895        format!("rb_ring_{}", id),
1896        if selected {
1897            config.selected_color
1898        } else {
1899            config.unselected_color
1900        },
1901        color_spec,
1902    );
1903    let dot_size = animate_f32(
1904        format!("rb_dot_{}", id),
1905        if selected {
1906            RadioButtonDefaults::DOT_RADIUS * 2.0
1907        } else {
1908            0.0
1909        },
1910        spring,
1911    );
1912
1913    Box(Modifier::new()
1914        .width(RadioButtonDefaults::TOUCH_TARGET_SIZE)
1915        .height(RadioButtonDefaults::TOUCH_TARGET_SIZE)
1916        .padding(0.0)
1917        .clip_rounded(20.0)
1918        .background(Color::TRANSPARENT)
1919        .clickable()
1920        .align_items(AlignItems::Center)
1921        .justify_content(JustifyContent::Center)
1922        .on_pointer_down(move |_| on_select())
1923        .then(config.modifier))
1924    .child(
1925        Box(Modifier::new()
1926            .size(d, d)
1927            .border(RadioButtonDefaults::STROKE_WIDTH, ring_col, d * 0.5)
1928            .clip_rounded(d * 0.5)
1929            .align_items(AlignItems::Center)
1930            .justify_content(JustifyContent::Center))
1931        .child(if dot_size > 0.5 {
1932            Box(Modifier::new()
1933                .size(dot_size, dot_size)
1934                .background(config.selected_color)
1935                .clip_rounded(dot_size * 0.5))
1936        } else {
1937            Box(Modifier::new())
1938        }),
1939    )
1940}
1941
1942/// Configuration for [`Switch`].
1943#[derive(Clone, Debug)]
1944pub struct SwitchConfig {
1945    pub modifier: Modifier,
1946    pub checked_track_color: Color,
1947    pub unchecked_track_color: Color,
1948    pub checked_thumb_color: Color,
1949    pub unchecked_thumb_color: Color,
1950    pub unchecked_border_color: Color,
1951}
1952
1953impl Default for SwitchConfig {
1954    fn default() -> Self {
1955        Self {
1956            modifier: Modifier::new(),
1957            checked_track_color: SwitchDefaults::checked_track_color(),
1958            unchecked_track_color: SwitchDefaults::unchecked_track_color(),
1959            checked_thumb_color: SwitchDefaults::checked_thumb_color(),
1960            unchecked_thumb_color: SwitchDefaults::unchecked_thumb_color(),
1961            unchecked_border_color: SwitchDefaults::unchecked_border_color(),
1962        }
1963    }
1964}
1965
1966/// M3 Switch.
1967/// Renders a pill track with an animated thumb knob.
1968/// Thumb position, size, and colors animate with spring/tween physics.
1969static SWITCH_COUNTER: AtomicU64 = AtomicU64::new(0);
1970pub fn Switch(checked: bool, on_change: impl Fn(bool) + 'static, config: SwitchConfig) -> View {
1971    let th = theme();
1972    let track_w = SwitchDefaults::TRACK_WIDTH;
1973    let track_h = SwitchDefaults::TRACK_HEIGHT;
1974
1975    let id = remember(|| SWITCH_COUNTER.fetch_add(1, Ordering::Relaxed));
1976
1977    // Thumb: spring-animated position and size
1978    let thumb_target_pos = if checked {
1979        track_w - SwitchDefaults::THUMB_CHECKED_SIZE - 4.0
1980    } else {
1981        8.0
1982    };
1983    let thumb_target_d = if checked {
1984        SwitchDefaults::THUMB_CHECKED_SIZE
1985    } else {
1986        SwitchDefaults::THUMB_UNCHECKED_SIZE
1987    };
1988    let spring = th.motion.spring;
1989
1990    let thumb_left = animate_f32(format!("sw_pos_{}", id), thumb_target_pos, spring);
1991    let thumb_d = animate_f32(format!("sw_d_{}", id), thumb_target_d, spring);
1992    let thumb_top = (track_h - thumb_d) * 0.5;
1993
1994    let color_spec = th.motion.color_fast;
1995    let track_bg = animate_color(
1996        format!("sw_tbg_{}", id),
1997        if checked {
1998            config.checked_track_color
1999        } else {
2000            config.unchecked_track_color
2001        },
2002        color_spec,
2003    );
2004    let thumb_bg = animate_color(
2005        format!("sw_tmbg_{}", id),
2006        if checked {
2007            config.checked_thumb_color
2008        } else {
2009            config.unchecked_thumb_color
2010        },
2011        color_spec,
2012    );
2013    let track_border = animate_f32(
2014        format!("sw_tb_{}", id),
2015        if checked { 0.0 } else { 2.0 },
2016        color_spec,
2017    );
2018    let border_color = animate_color(
2019        format!("sw_bc_{}", id),
2020        if checked {
2021            Color::TRANSPARENT
2022        } else {
2023            config.unchecked_border_color
2024        },
2025        color_spec,
2026    );
2027
2028    Box(Modifier::new()
2029        .size(track_w, track_h)
2030        .padding(0.0)
2031        .clip_rounded(track_h * 0.5)
2032        .background(Color::TRANSPARENT)
2033        .clickable()
2034        .on_pointer_down(move |_| on_change(!checked))
2035        .then(config.modifier))
2036    .child(
2037        Box(Modifier::new()
2038            .size(track_w, track_h)
2039            .background(track_bg)
2040            .border(track_border, border_color, track_h * 0.5)
2041            .clip_rounded(track_h * 0.5))
2042        .child(Box(Modifier::new()
2043            .size(thumb_d, thumb_d)
2044            .background(thumb_bg)
2045            .clip_rounded(thumb_d * 0.5)
2046            .absolute()
2047            .offset(Some(thumb_left), Some(thumb_top), None, None))),
2048    )
2049}
2050
2051/// Configuration for [`M3Slider`] and [`M3RangeSlider`].
2052#[derive(Clone, Debug)]
2053pub struct SliderConfig {
2054    pub modifier: Modifier,
2055    pub active_track_color: Color,
2056    pub inactive_track_color: Color,
2057    pub thumb_color: Color,
2058}
2059
2060impl Default for SliderConfig {
2061    fn default() -> Self {
2062        Self {
2063            modifier: Modifier::new(),
2064            active_track_color: SliderDefaults::active_track_color(),
2065            inactive_track_color: SliderDefaults::inactive_track_color(),
2066            thumb_color: SliderDefaults::thumb_color(),
2067        }
2068    }
2069}
2070
2071static SLIDER_COUNTER: AtomicU64 = AtomicU64::new(0);
2072
2073fn snap_step(v: f32, min: f32, max: f32, step: Option<f32>) -> f32 {
2074    let v = v.clamp(min, max);
2075    if let Some(s) = step.filter(|s| *s > 0.0) {
2076        let t = ((v - min) / s).round();
2077        (min + t * s).clamp(min, max)
2078    } else {
2079        v
2080    }
2081}
2082
2083fn value_from_x(x: f32, rect: Rect, min: f32, max: f32, step: Option<f32>) -> f32 {
2084    let w = rect.w.max(1.0);
2085    let t = ((x - rect.x) / w).clamp(0.0, 1.0);
2086    let v = min + t * (max - min);
2087    snap_step(v, min, max, step)
2088}
2089
2090pub fn M3Slider(
2091    value: f32,
2092    range: (f32, f32),
2093    step: Option<f32>,
2094    on_change: impl Fn(f32) + 'static,
2095    config: SliderConfig,
2096) -> View {
2097    let id = *remember(|| SLIDER_COUNTER.fetch_add(1, Ordering::Relaxed));
2098    let track_rect = remember_state_with_key(format!("ms_rect_{}", id), || Rect::default());
2099    let drag_active = remember_state_with_key(format!("ms_da_{}", id), || false);
2100
2101    let track_rect_p = track_rect.clone();
2102    let drag_active_p = drag_active.clone();
2103
2104    let min = range.0;
2105    let max = range.1;
2106    let oc = Rc::new(on_change);
2107    let range_size = (max - min).max(1e-6);
2108    let t = ((value - min) / range_size).clamp(0.0, 1.0);
2109
2110    let tick_frac: Vec<f32> = if let Some(s) = step {
2111        let n = ((max - min) / s.max(1e-6)).round() as usize;
2112        (0..=n).map(|i| i as f32 / n as f32).collect()
2113    } else {
2114        Vec::new()
2115    };
2116
2117    Box(Modifier::new()
2118        .min_width(200.0)
2119        .height(44.0)
2120        .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
2121            let mul_c = |c: Color| {
2122                Color(
2123                    c.0,
2124                    c.1,
2125                    c.2,
2126                    ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
2127                )
2128            };
2129            let track_h = dp_to_px(16.0);
2130            let thumb_w = dp_to_px(4.0);
2131            let thumb_h = dp_to_px(44.0);
2132            let dot_r = dp_to_px(2.0);
2133            let corner = track_h * 0.5;
2134            let gap = dp_to_px(8.0);
2135            let pad = thumb_w * 0.5;
2136            let track_x = rect.x + pad;
2137            let track_w = (rect.w - thumb_w).max(0.0);
2138            let cy = rect.y + rect.h * 0.5;
2139
2140            // Thumb center: for steps, clamp within corner inset (Compose SliderImpl:924-930)
2141            let kx = if step.is_some() && !tick_frac.is_empty() {
2142                let is_first = (t - tick_frac[0]).abs() < 1e-6;
2143                let is_last = (t - tick_frac[tick_frac.len() - 1]).abs() < 1e-6;
2144                if is_first || is_last {
2145                    track_x + t * track_w
2146                } else {
2147                    track_x + (track_w - track_h) * t + corner
2148                }
2149            } else {
2150                track_x + t * track_w
2151            };
2152
2153            *track_rect_p.borrow_mut() = Rect {
2154                x: track_x,
2155                y: rect.y,
2156                w: track_w,
2157                h: rect.h,
2158            };
2159
2160            // Inactive track (after thumb gap)
2161            let inactive_x = track_x.max(kx + gap);
2162            let inactive_w = (track_x + track_w - inactive_x).max(0.0);
2163            if inactive_w > 0.0 {
2164                scene.nodes.push(SceneNode::Rect {
2165                    rect: Rect {
2166                        x: inactive_x,
2167                        y: cy - track_h * 0.5,
2168                        w: inactive_w,
2169                        h: track_h,
2170                    },
2171                    brush: Brush::Solid(mul_c(config.inactive_track_color)),
2172                    radius: corner,
2173                });
2174            }
2175            // Active track fill (from left to thumb gap)
2176            let fill_w = (kx - gap - track_x).max(0.0);
2177            if fill_w > 0.0 {
2178                scene.nodes.push(SceneNode::Rect {
2179                    rect: Rect {
2180                        x: track_x,
2181                        y: cy - track_h * 0.5,
2182                        w: fill_w,
2183                        h: track_h,
2184                    },
2185                    brush: Brush::Solid(mul_c(config.active_track_color)),
2186                    radius: corner,
2187                });
2188            }
2189            // Tick marks at step positions (skipping gap region)
2190            let tick_start = track_x + corner;
2191            let tick_end = track_x + track_w - corner;
2192            for &tf in &tick_frac {
2193                let tx = tick_start + tf * (tick_end - tick_start);
2194                if tx >= kx - gap && tx <= kx + gap {
2195                    continue;
2196                }
2197                let on_active = tx <= kx - gap;
2198                scene.nodes.push(SceneNode::Ellipse {
2199                    rect: Rect {
2200                        x: tx - dot_r,
2201                        y: cy - dot_r,
2202                        w: dot_r * 2.0,
2203                        h: dot_r * 2.0,
2204                    },
2205                    brush: Brush::Solid(mul_c(if on_active {
2206                        config.inactive_track_color
2207                    } else {
2208                        config.active_track_color
2209                    })),
2210                });
2211            }
2212            // Stop indicator at right end (only when inactive track visible)
2213            if inactive_w > 0.0 {
2214                let sx = track_x + track_w - corner;
2215                scene.nodes.push(SceneNode::Ellipse {
2216                    rect: Rect {
2217                        x: sx - dot_r,
2218                        y: cy - dot_r,
2219                        w: dot_r * 2.0,
2220                        h: dot_r * 2.0,
2221                    },
2222                    brush: Brush::Solid(mul_c(config.active_track_color)),
2223                });
2224            }
2225            // Thumb pill (shrinks to 2dp when dragging, matching Compose Thumb:2469-2478)
2226            let da = *drag_active_p.borrow();
2227            let tw = if da { thumb_w * 0.5 } else { thumb_w };
2228            scene.nodes.push(SceneNode::Rect {
2229                rect: Rect {
2230                    x: kx - tw * 0.5,
2231                    y: cy - thumb_h * 0.5,
2232                    w: tw,
2233                    h: thumb_h,
2234                },
2235                brush: Brush::Solid(mul_c(config.thumb_color)),
2236                radius: tw * 0.5,
2237            });
2238        })
2239        .on_pointer_down({
2240            let oc = oc.clone();
2241            let track_rect = track_rect.clone();
2242            let drag_active = drag_active.clone();
2243            move |pe: PointerEvent| {
2244                *drag_active.borrow_mut() = true;
2245                let r = *track_rect.borrow();
2246                (oc)(value_from_x(pe.position.x, r, min, max, step));
2247            }
2248        })
2249        .on_pointer_move({
2250            let oc = oc.clone();
2251            let track_rect = track_rect.clone();
2252            let drag_active = drag_active.clone();
2253            move |pe: PointerEvent| {
2254                if !*drag_active.borrow() {
2255                    return;
2256                }
2257                let r = *track_rect.borrow();
2258                (oc)(value_from_x(pe.position.x, r, min, max, step));
2259            }
2260        })
2261        .on_pointer_up(move |_pe: PointerEvent| {
2262            *drag_active.borrow_mut() = false;
2263        })
2264        .on_scroll({
2265            let oc = oc.clone();
2266            move |d: Vec2| -> Vec2 {
2267                let dir = if d.y < -0.5 {
2268                    1
2269                } else if d.y > 0.5 {
2270                    -1
2271                } else {
2272                    0
2273                };
2274                if dir == 0 {
2275                    return d;
2276                }
2277                let step_val = step.unwrap_or(1.0).max(1e-6);
2278                let new_val = snap_step(value + (dir as f32) * step_val, min, max, step);
2279                if (new_val - value).abs() > 1e-6 {
2280                    (oc)(new_val);
2281                    Vec2 { x: d.x, y: 0.0 }
2282                } else {
2283                    d
2284                }
2285            }
2286        })
2287        .then(config.modifier))
2288    .semantics(Semantics {
2289        role: Role::Slider,
2290        label: None,
2291        focused: false,
2292        enabled: true,
2293    })
2294}
2295
2296pub fn M3RangeSlider(
2297    start: f32,
2298    end: f32,
2299    range: (f32, f32),
2300    step: Option<f32>,
2301    on_change: impl Fn(f32, f32) + 'static,
2302    config: SliderConfig,
2303) -> View {
2304    let id = *remember(|| SLIDER_COUNTER.fetch_add(1, Ordering::Relaxed));
2305    let track_rect = remember_state_with_key(format!("mrs_rect_{}", id), || Rect::default());
2306    let drag_active = remember_state_with_key(format!("mrs_da_{}", id), || false);
2307    let active_thumb = remember_state_with_key(format!("mrs_at_{}", id), || false);
2308
2309    let min = range.0;
2310    let max = range.1;
2311    let oc = Rc::new(on_change);
2312    let range_size = (max - min).max(1e-6);
2313    let t0 = ((start - min) / range_size).clamp(0.0, 1.0);
2314    let t1 = ((end - min) / range_size).clamp(0.0, 1.0);
2315
2316    let tick_frac: Vec<f32> = if let Some(s) = step {
2317        let n = ((max - min) / s.max(1e-6)).round() as usize;
2318        (0..=n).map(|i| i as f32 / n as f32).collect()
2319    } else {
2320        Vec::new()
2321    };
2322
2323    let track_rect_p = track_rect.clone();
2324    let drag_active_p = drag_active.clone();
2325    let active_thumb_p = active_thumb.clone();
2326
2327    Box(Modifier::new()
2328        .min_width(200.0)
2329        .height(44.0)
2330        .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
2331            let mul_c = |c: Color| {
2332                Color(
2333                    c.0,
2334                    c.1,
2335                    c.2,
2336                    ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
2337                )
2338            };
2339            let track_h = dp_to_px(16.0);
2340            let thumb_w = dp_to_px(4.0);
2341            let thumb_h = dp_to_px(44.0);
2342            let dot_r = dp_to_px(2.0);
2343            let corner = track_h * 0.5;
2344            let gap = dp_to_px(8.0);
2345            let pad = thumb_w * 0.5;
2346            let track_x = rect.x + pad;
2347            let track_w = (rect.w - thumb_w).max(0.0);
2348            let cy = rect.y + rect.h * 0.5;
2349
2350            // Thumb centers with corner-inset for steps
2351            let thumb_pos = |tf: f32, fracs: &[f32]| {
2352                if step.is_some() && !fracs.is_empty() {
2353                    let is_first = (tf - fracs[0]).abs() < 1e-6;
2354                    let is_last = (tf - fracs[fracs.len() - 1]).abs() < 1e-6;
2355                    if is_first || is_last {
2356                        track_x + tf * track_w
2357                    } else {
2358                        track_x + (track_w - track_h) * tf + corner
2359                    }
2360                } else {
2361                    track_x + tf * track_w
2362                }
2363            };
2364            let k0 = thumb_pos(t0, &tick_frac);
2365            let k1 = thumb_pos(t1, &tick_frac);
2366            let active_l = k0.min(k1);
2367            let active_r = k0.max(k1);
2368
2369            *track_rect_p.borrow_mut() = Rect {
2370                x: track_x,
2371                y: rect.y,
2372                w: track_w,
2373                h: rect.h,
2374            };
2375
2376            let linactive_w = (active_l - gap - track_x).max(0.0);
2377            if linactive_w > 0.0 {
2378                scene.nodes.push(SceneNode::Rect {
2379                    rect: Rect {
2380                        x: track_x,
2381                        y: cy - track_h * 0.5,
2382                        w: linactive_w,
2383                        h: track_h,
2384                    },
2385                    brush: Brush::Solid(mul_c(config.inactive_track_color)),
2386                    radius: corner,
2387                });
2388            }
2389            let rinactive_x = (active_r + gap).min(track_x + track_w);
2390            let rinactive_w = (track_x + track_w - rinactive_x).max(0.0);
2391            if rinactive_w > 0.0 {
2392                scene.nodes.push(SceneNode::Rect {
2393                    rect: Rect {
2394                        x: rinactive_x,
2395                        y: cy - track_h * 0.5,
2396                        w: rinactive_w,
2397                        h: track_h,
2398                    },
2399                    brush: Brush::Solid(mul_c(config.inactive_track_color)),
2400                    radius: corner,
2401                });
2402            }
2403            // Active range between thumbs
2404            let active_w = (active_r - gap - (active_l + gap)).max(0.0);
2405            if active_w > 0.0 {
2406                scene.nodes.push(SceneNode::Rect {
2407                    rect: Rect {
2408                        x: active_l + gap,
2409                        y: cy - track_h * 0.5,
2410                        w: active_w,
2411                        h: track_h,
2412                    },
2413                    brush: Brush::Solid(mul_c(config.active_track_color)),
2414                    radius: corner,
2415                });
2416            }
2417            // Tick marks at step positions (skipping gap regions)
2418            let tick_start = track_x + corner;
2419            let tick_end = track_x + track_w - corner;
2420            for &tf in &tick_frac {
2421                let tx = tick_start + tf * (tick_end - tick_start);
2422                if tx >= active_l - gap && tx <= active_r + gap {
2423                    continue;
2424                }
2425                let on_active = tx >= active_l + gap && tx <= active_r - gap;
2426                scene.nodes.push(SceneNode::Ellipse {
2427                    rect: Rect {
2428                        x: tx - dot_r,
2429                        y: cy - dot_r,
2430                        w: dot_r * 2.0,
2431                        h: dot_r * 2.0,
2432                    },
2433                    brush: Brush::Solid(mul_c(if on_active {
2434                        config.inactive_track_color
2435                    } else {
2436                        config.active_track_color
2437                    })),
2438                });
2439            }
2440            // Stop indicators (only when corresponding inactive track visible)
2441            if linactive_w > 0.0 {
2442                let sx0 = track_x + corner;
2443                scene.nodes.push(SceneNode::Ellipse {
2444                    rect: Rect {
2445                        x: sx0 - dot_r,
2446                        y: cy - dot_r,
2447                        w: dot_r * 2.0,
2448                        h: dot_r * 2.0,
2449                    },
2450                    brush: Brush::Solid(mul_c(config.active_track_color)),
2451                });
2452            }
2453            if rinactive_w > 0.0 {
2454                let sx = track_x + track_w - corner;
2455                scene.nodes.push(SceneNode::Ellipse {
2456                    rect: Rect {
2457                        x: sx - dot_r,
2458                        y: cy - dot_r,
2459                        w: dot_r * 2.0,
2460                        h: dot_r * 2.0,
2461                    },
2462                    brush: Brush::Solid(mul_c(config.active_track_color)),
2463                });
2464            }
2465            // Thumb pills (shrink to 2dp when dragging that specific thumb)
2466            let da = *drag_active_p.borrow();
2467            let at = *active_thumb_p.borrow();
2468            let thumb_sizes = [
2469                (k0, if da && !at { thumb_w * 0.5 } else { thumb_w }),
2470                (k1, if da && at { thumb_w * 0.5 } else { thumb_w }),
2471            ];
2472            for &(kx, tw) in &thumb_sizes {
2473                scene.nodes.push(SceneNode::Rect {
2474                    rect: Rect {
2475                        x: kx - tw * 0.5,
2476                        y: cy - thumb_h * 0.5,
2477                        w: tw,
2478                        h: thumb_h,
2479                    },
2480                    brush: Brush::Solid(mul_c(config.thumb_color)),
2481                    radius: tw * 0.5,
2482                });
2483            }
2484        })
2485        .on_pointer_down({
2486            let oc = oc.clone();
2487            let track_rect = track_rect.clone();
2488            let drag_active = drag_active.clone();
2489            let active_thumb = active_thumb.clone();
2490            move |pe: PointerEvent| {
2491                *drag_active.borrow_mut() = true;
2492                let r = *track_rect.borrow();
2493                let v = value_from_x(pe.position.x, r, min, max, step);
2494                let use_end = (v - end).abs() < (v - start).abs();
2495                *active_thumb.borrow_mut() = use_end;
2496                let (a, b) = if use_end {
2497                    (start, v.max(start))
2498                } else {
2499                    (v.min(end), end)
2500                };
2501                (oc)(a, b);
2502            }
2503        })
2504        .on_pointer_move({
2505            let oc = oc.clone();
2506            let track_rect = track_rect.clone();
2507            let drag_active = drag_active.clone();
2508            let active_thumb = active_thumb.clone();
2509            move |pe: PointerEvent| {
2510                if !*drag_active.borrow() {
2511                    return;
2512                }
2513                let r = *track_rect.borrow();
2514                let v = value_from_x(pe.position.x, r, min, max, step);
2515                let use_end = *active_thumb.borrow();
2516                let (a, b) = if use_end {
2517                    (start, v.max(start))
2518                } else {
2519                    (v.min(end), end)
2520                };
2521                (oc)(a, b);
2522            }
2523        })
2524        .on_pointer_up({
2525            let drag_active = drag_active.clone();
2526            let active_thumb = active_thumb.clone();
2527            move |_pe: PointerEvent| {
2528                *drag_active.borrow_mut() = false;
2529                *active_thumb.borrow_mut() = false;
2530            }
2531        })
2532        .on_scroll({
2533            let oc = oc.clone();
2534            let active_thumb = active_thumb.clone();
2535            move |d: Vec2| -> Vec2 {
2536                let dir = if d.y < -0.5 {
2537                    1
2538                } else if d.y > 0.5 {
2539                    -1
2540                } else {
2541                    0
2542                };
2543                if dir == 0 {
2544                    return d;
2545                }
2546                let step_val = step.unwrap_or(1.0).max(1e-6);
2547                let use_end = *active_thumb.borrow();
2548                let (mut a, mut b) = (start, end);
2549                if use_end {
2550                    b = snap_step(end + (dir as f32) * step_val, min, max, step).max(a);
2551                } else {
2552                    a = snap_step(start + (dir as f32) * step_val, min, max, step).min(b);
2553                }
2554                if (a - start).abs() > 1e-6 || (b - end).abs() > 1e-6 {
2555                    (oc)(a, b);
2556                    Vec2 { x: d.x, y: 0.0 }
2557                } else {
2558                    d
2559                }
2560            }
2561        })
2562        .then(config.modifier))
2563    .semantics(Semantics {
2564        role: Role::Slider,
2565        label: None,
2566        focused: false,
2567        enabled: true,
2568    })
2569}
2570
2571/// Configuration for [`Card`].
2572#[derive(Clone, Debug)]
2573pub struct CardConfig {
2574    pub modifier: Modifier,
2575    pub container_color: Color,
2576    pub shape_radius: f32,
2577    pub tonal_elevation: f32,
2578}
2579
2580impl Default for CardConfig {
2581    fn default() -> Self {
2582        Self {
2583            modifier: Modifier::new(),
2584            container_color: CardDefaults::filled_container_color(),
2585            shape_radius: CardDefaults::SHAPE_RADIUS,
2586            tonal_elevation: CardDefaults::ELEVATION,
2587        }
2588    }
2589}
2590
2591/// M3 Card - a configurable container surface.
2592pub fn Card(config: CardConfig, content: impl FnOnce() -> View) -> View {
2593    let mut m = Modifier::new()
2594        .background(config.container_color)
2595        .clip_rounded(config.shape_radius)
2596        .then(config.modifier);
2597    if config.tonal_elevation > 0.0 {
2598        m = m.state_elevation(StateElevation {
2599            default: config.tonal_elevation,
2600            hovered: config.tonal_elevation,
2601            pressed: config.tonal_elevation,
2602            disabled: 0.0,
2603        });
2604    }
2605    Box(m).child(content())
2606}
2607
2608/// Configuration for [`Snackbar`].
2609#[derive(Clone, Debug)]
2610pub struct SnackbarConfig {
2611    pub modifier: Modifier,
2612    pub container_color: Color,
2613    pub content_color: Color,
2614    pub action_color: Color,
2615    pub min_height: f32,
2616    pub min_width: f32,
2617    pub max_width: f32,
2618}
2619
2620impl Default for SnackbarConfig {
2621    fn default() -> Self {
2622        Self {
2623            modifier: Modifier::new(),
2624            container_color: SnackbarDefaults::container_color(),
2625            content_color: SnackbarDefaults::content_color(),
2626            action_color: SnackbarDefaults::action_color(),
2627            min_height: SnackbarDefaults::MIN_HEIGHT,
2628            min_width: SnackbarDefaults::MIN_WIDTH,
2629            max_width: SnackbarDefaults::MAX_WIDTH,
2630        }
2631    }
2632}
2633
2634/// Configuration for chips.
2635#[derive(Clone, Debug)]
2636pub struct ChipConfig {
2637    pub modifier: Modifier,
2638    pub enabled: bool,
2639    pub selected: bool,
2640    pub container_color: Color,
2641    pub selected_container_color: Color,
2642    pub content_color: Color,
2643    pub selected_content_color: Color,
2644    pub border_color: Color,
2645    pub shape_radius: f32,
2646    pub horizontal_padding: f32,
2647}
2648
2649impl Default for ChipConfig {
2650    fn default() -> Self {
2651        Self {
2652            modifier: Modifier::new(),
2653            enabled: true,
2654            selected: false,
2655            container_color: ChipDefaults::surface_color(),
2656            selected_container_color: ChipDefaults::selected_container_color(),
2657            content_color: ChipDefaults::unselected_content_color(),
2658            selected_content_color: ChipDefaults::selected_content_color(),
2659            border_color: ChipDefaults::unselected_border_color(),
2660            shape_radius: ChipDefaults::SHAPE_RADIUS,
2661            horizontal_padding: ChipDefaults::HORIZONTAL_PADDING,
2662        }
2663    }
2664}
2665
2666/// M3 Assist Chip - a chip for triggering actions.
2667pub fn AssistChip(
2668    on_click: impl Fn() + 'static,
2669    label: View,
2670    leading_icon: Option<View>,
2671    trailing_icon: Option<View>,
2672    config: ChipConfig,
2673) -> View {
2674    let th = theme();
2675    let shape = config.shape_radius;
2676    Box(Modifier::new()
2677        .state_colors(StateColors {
2678            default: Color::TRANSPARENT,
2679            hovered: th.on_surface.with_alpha_f32(0.08),
2680            pressed: th.on_surface.with_alpha_f32(0.12),
2681            disabled: Color::TRANSPARENT,
2682        })
2683        .padding_values(PaddingValues {
2684            left: config.horizontal_padding,
2685            right: config.horizontal_padding,
2686            top: 8.0,
2687            bottom: 8.0,
2688        })
2689        .clickable()
2690        .on_pointer_down(move |_| on_click())
2691        .background(Color::TRANSPARENT)
2692        .clip_rounded(shape)
2693        .border(1.0, config.border_color, shape)
2694        .then(config.modifier))
2695    .child(
2696        Row(Modifier::new().align_items(AlignItems::Center)).child((
2697            leading_icon
2698                .map(|v| {
2699                    Box(Modifier::new().padding_values(PaddingValues {
2700                        left: 0.0,
2701                        right: 8.0,
2702                        top: 0.0,
2703                        bottom: 0.0,
2704                    }))
2705                    .child(with_content_color(config.content_color, move || v))
2706                })
2707                .unwrap_or(Box(Modifier::new())),
2708            with_content_color(config.content_color, move || label),
2709            trailing_icon
2710                .map(|v| {
2711                    Box(Modifier::new().padding_values(PaddingValues {
2712                        left: 8.0,
2713                        right: 0.0,
2714                        top: 0.0,
2715                        bottom: 0.0,
2716                    }))
2717                    .child(with_content_color(config.content_color, move || v))
2718                })
2719                .unwrap_or(Box(Modifier::new())),
2720        )),
2721    )
2722}
2723
2724/// Configuration for [`NavigationBar`].
2725#[derive(Clone, Debug)]
2726pub struct NavigationBarConfig {
2727    pub modifier: Modifier,
2728    pub container_color: Color,
2729    pub selected_icon_color: Color,
2730    pub unselected_icon_color: Color,
2731    pub indicator_color: Color,
2732    pub height: f32,
2733    pub indicator_opacity: f32,
2734    pub item_horizontal_padding: f32,
2735    pub item_vertical_padding: f32,
2736    pub indicator_radius: f32,
2737}
2738
2739impl Default for NavigationBarConfig {
2740    fn default() -> Self {
2741        Self {
2742            modifier: Modifier::new(),
2743            container_color: NavigationBarDefaults::container_color(),
2744            selected_icon_color: NavigationBarDefaults::selected_icon_color(),
2745            unselected_icon_color: NavigationBarDefaults::unselected_icon_color(),
2746            indicator_color: NavigationBarDefaults::indicator_color(),
2747            height: NavigationBarDefaults::HEIGHT,
2748            indicator_opacity: NavigationBarDefaults::ITEM_ACTIVE_INDICATOR_OPACITY,
2749            item_horizontal_padding: 4.0,
2750            item_vertical_padding: 6.0,
2751            indicator_radius: 16.0,
2752        }
2753    }
2754}
2755
2756/// Configuration for [`NavigationRail`].
2757#[derive(Clone, Debug)]
2758pub struct NavigationRailConfig {
2759    pub modifier: Modifier,
2760    pub container_color: Color,
2761    pub selected_icon_color: Color,
2762    pub unselected_icon_color: Color,
2763    pub selected_container_color: Color,
2764    pub width: f32,
2765    pub item_radius: f32,
2766}
2767
2768impl Default for NavigationRailConfig {
2769    fn default() -> Self {
2770        Self {
2771            modifier: Modifier::new(),
2772            container_color: NavigationRailDefaults::container_color(),
2773            selected_icon_color: NavigationRailDefaults::selected_icon_color(),
2774            unselected_icon_color: NavigationRailDefaults::unselected_icon_color(),
2775            selected_container_color: NavigationRailDefaults::selected_container_color(),
2776            width: NavigationRailDefaults::WIDTH,
2777            item_radius: 16.0,
2778        }
2779    }
2780}
2781
2782/// Configuration for [`Scaffold`].
2783#[derive(Clone, Debug)]
2784pub struct ScaffoldConfig {
2785    pub modifier: Modifier,
2786    pub container_color: Color,
2787    pub top_bar_height: f32,
2788    pub bottom_bar_height: f32,
2789    pub fab_margin: f32,
2790}
2791
2792impl Default for ScaffoldConfig {
2793    fn default() -> Self {
2794        Self {
2795            modifier: Modifier::new(),
2796            container_color: ScaffoldDefaults::container_color(),
2797            top_bar_height: ScaffoldDefaults::TOP_BAR_HEIGHT,
2798            bottom_bar_height: ScaffoldDefaults::BOTTOM_BAR_HEIGHT,
2799            fab_margin: ScaffoldDefaults::FAB_MARGIN,
2800        }
2801    }
2802}
2803
2804/// Configuration for [`NavigationDrawer`].
2805#[derive(Clone, Debug)]
2806pub struct NavigationDrawerConfig {
2807    pub modifier: Modifier,
2808    pub container_color: Color,
2809    pub scrim_color: Color,
2810    pub width: f32,
2811    pub shape_radius: f32,
2812}
2813
2814impl Default for NavigationDrawerConfig {
2815    fn default() -> Self {
2816        Self {
2817            modifier: Modifier::new(),
2818            container_color: NavigationDrawerDefaults::container_color(),
2819            scrim_color: NavigationDrawerDefaults::scrim_color(),
2820            width: NavigationDrawerDefaults::WIDTH,
2821            shape_radius: NavigationDrawerDefaults::SHAPE_RADIUS,
2822        }
2823    }
2824}
2825
2826/// Configuration for [`BottomSheet`] / `ModalBottomSheet`.
2827#[derive(Clone, Debug)]
2828pub struct BottomSheetConfig {
2829    pub modifier: Modifier,
2830    pub container_color: Color,
2831    pub scrim_color: Color,
2832    pub drag_handle_color: Color,
2833    pub shape_radius: f32,
2834    pub max_width: f32,
2835    pub drag_handle_width: f32,
2836    pub drag_handle_height: f32,
2837    pub peek_height: f32,
2838}
2839
2840impl Default for BottomSheetConfig {
2841    fn default() -> Self {
2842        Self {
2843            modifier: Modifier::new(),
2844            container_color: BottomSheetDefaults::container_color(),
2845            scrim_color: BottomSheetDefaults::scrim_color(),
2846            drag_handle_color: BottomSheetDefaults::drag_handle_color(),
2847            shape_radius: BottomSheetDefaults::SHAPE_RADIUS,
2848            max_width: BottomSheetDefaults::MAX_WIDTH,
2849            drag_handle_width: BottomSheetDefaults::DRAG_HANDLE_WIDTH,
2850            drag_handle_height: BottomSheetDefaults::DRAG_HANDLE_HEIGHT,
2851            peek_height: BottomSheetDefaults::PEEK_HEIGHT,
2852        }
2853    }
2854}
2855
2856/// Configuration for [`SearchBar`].
2857#[derive(Clone, Debug)]
2858pub struct SearchBarConfig {
2859    pub modifier: Modifier,
2860    pub container_color: Color,
2861    pub active_container_color: Color,
2862    pub content_color: Color,
2863    pub placeholder_color: Color,
2864    pub height: f32,
2865    pub expanded_width: f32,
2866    pub collapsed_width: f32,
2867}
2868
2869impl Default for SearchBarConfig {
2870    fn default() -> Self {
2871        Self {
2872            modifier: Modifier::new(),
2873            container_color: SearchBarDefaults::container_color(),
2874            active_container_color: SearchBarDefaults::active_container_color(),
2875            content_color: SearchBarDefaults::content_color(),
2876            placeholder_color: SearchBarDefaults::placeholder_color(),
2877            height: SearchBarDefaults::HEIGHT,
2878            expanded_width: SearchBarDefaults::EXPANDED_WIDTH,
2879            collapsed_width: SearchBarDefaults::COLLAPSED_WIDTH,
2880        }
2881    }
2882}
2883
2884/// Configuration for [`DropdownMenu`].
2885#[derive(Clone, Debug)]
2886pub struct DropdownMenuConfig {
2887    pub modifier: Modifier,
2888    pub container_color: Color,
2889    pub item_text_color: Color,
2890    pub disabled_item_text_color: Color,
2891    pub divider_color: Color,
2892    pub min_width: f32,
2893    pub item_height: f32,
2894}
2895
2896impl Default for DropdownMenuConfig {
2897    fn default() -> Self {
2898        Self {
2899            modifier: Modifier::new(),
2900            container_color: DropdownMenuDefaults::container_color(),
2901            item_text_color: DropdownMenuDefaults::item_text_color(),
2902            disabled_item_text_color: DropdownMenuDefaults::disabled_item_text_color(),
2903            divider_color: DropdownMenuDefaults::divider_color(),
2904            min_width: DropdownMenuDefaults::MIN_WIDTH,
2905            item_height: DropdownMenuDefaults::ITEM_HEIGHT,
2906        }
2907    }
2908}
2909
2910/// Configuration for tooltip.
2911#[derive(Clone, Debug)]
2912pub struct TooltipConfig {
2913    pub modifier: Modifier,
2914    pub container_color: Color,
2915    pub content_color: Color,
2916    pub offset_y: f32,
2917    pub horizontal_padding: f32,
2918    pub vertical_padding: f32,
2919}
2920
2921impl Default for TooltipConfig {
2922    fn default() -> Self {
2923        Self {
2924            modifier: Modifier::new(),
2925            container_color: TooltipDefaults::container_color(),
2926            content_color: TooltipDefaults::content_color(),
2927            offset_y: TooltipDefaults::OFFSET_Y,
2928            horizontal_padding: TooltipDefaults::HORIZONTAL_PADDING,
2929            vertical_padding: TooltipDefaults::VERTICAL_PADDING,
2930        }
2931    }
2932}
2933
2934/// Configuration for swipe-to-dismiss.
2935#[derive(Clone, Debug)]
2936pub struct SwipeToDismissConfig {
2937    pub modifier: Modifier,
2938    pub dismiss_threshold: f32,
2939    pub dismissed_offset: f32,
2940    pub animation_spec: AnimationSpec,
2941}
2942
2943impl Default for SwipeToDismissConfig {
2944    fn default() -> Self {
2945        Self {
2946            modifier: Modifier::new(),
2947            dismiss_threshold: 150.0,
2948            dismissed_offset: 300.0,
2949            animation_spec: AnimationSpec::spring_gentle(),
2950        }
2951    }
2952}
2953
2954/// Configuration for pull-to-refresh.
2955#[derive(Clone, Debug)]
2956pub struct PullToRefreshConfig {
2957    pub modifier: Modifier,
2958    pub indicator_color: Color,
2959    pub threshold: f32,
2960}
2961
2962impl Default for PullToRefreshConfig {
2963    fn default() -> Self {
2964        Self {
2965            modifier: Modifier::new(),
2966            indicator_color: PullToRefreshDefaults::indicator_color(),
2967            threshold: PullToRefreshDefaults::THRESHOLD,
2968        }
2969    }
2970}
2971
2972/// Configuration for alert dialog.
2973#[derive(Clone, Debug)]
2974pub struct AlertDialogConfig {
2975    pub modifier: Modifier,
2976    pub scrim_color: Color,
2977    pub min_width: f32,
2978    pub max_width: f32,
2979    pub horizontal_padding: f32,
2980}
2981
2982impl Default for AlertDialogConfig {
2983    fn default() -> Self {
2984        Self {
2985            modifier: Modifier::new(),
2986            scrim_color: AlertDialogDefaults::scrim_color(),
2987            min_width: AlertDialogDefaults::MIN_WIDTH,
2988            max_width: AlertDialogDefaults::MAX_WIDTH,
2989            horizontal_padding: AlertDialogDefaults::HORIZONTAL_PADDING,
2990        }
2991    }
2992}