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};
6use web_time::Duration;
7
8use repose_core::animation::{AnimationSpec, CubicBezier, Easing, KeyframesSpec, RepeatableSpec};
9use repose_core::*;
10use repose_ui::anim::{animate_color, animate_f32};
11use repose_ui::{Box, Column, Row, Text, TextStyle, ViewExt};
12
13use super::*;
14
15use crate::ripple::{ripple, RippleConfig};
16use crate::{Icon, Symbol};
17
18/// Color slots for [`TopAppBar`].
19#[derive(Clone, Copy, Debug)]
20pub struct TopAppBarColors {
21    pub container_color: Color,
22    pub navigation_icon_content_color: Color,
23    pub title_content_color: Color,
24    pub action_icon_content_color: Color,
25}
26
27impl Default for TopAppBarColors {
28    fn default() -> Self {
29        Self {
30            container_color: TopAppBarDefaults::container_color(),
31            navigation_icon_content_color: TopAppBarDefaults::navigation_icon_content_color(),
32            title_content_color: TopAppBarDefaults::title_content_color(),
33            action_icon_content_color: TopAppBarDefaults::action_icon_content_color(),
34        }
35    }
36}
37
38/// Configuration for [`TopAppBar`].
39#[derive(Clone, Debug)]
40pub struct TopAppBarConfig {
41    pub modifier: Modifier,
42    pub colors: TopAppBarColors,
43    pub height: f32,
44    pub window_insets: WindowInsets,
45    pub content_padding: PaddingValues,
46}
47
48/// System window insets for top app bar padding.
49#[derive(Clone, Copy, Debug)]
50pub struct WindowInsets {
51    pub top: f32,
52    pub bottom: f32,
53    pub left: f32,
54    pub right: f32,
55}
56
57impl Default for WindowInsets {
58    fn default() -> Self {
59        Self {
60            top: 0.0,
61            bottom: 0.0,
62            left: 0.0,
63            right: 0.0,
64        }
65    }
66}
67
68impl Default for TopAppBarConfig {
69    fn default() -> Self {
70        Self {
71            modifier: Modifier::new(),
72            colors: TopAppBarColors::default(),
73            height: TopAppBarDefaults::HEIGHT,
74            window_insets: WindowInsets::default(),
75            content_padding: PaddingValues {
76                left: 4.0,
77                right: 4.0,
78                top: 0.0,
79                bottom: 0.0,
80            },
81        }
82    }
83}
84
85fn top_app_bar_layout(
86    title: View,
87    navigation_icon: Option<View>,
88    actions: Vec<View>,
89    config: TopAppBarConfig,
90    centered: bool,
91) -> View {
92    let insets = config.window_insets;
93    let mut m = Modifier::new()
94        .min_width(200.0)
95        .height(config.height + insets.top)
96        .background(config.colors.container_color)
97        .padding_values(PaddingValues {
98            left: config.content_padding.left + insets.left,
99            right: config.content_padding.right + insets.right,
100            top: config.content_padding.top + insets.top,
101            bottom: config.content_padding.bottom + insets.bottom,
102        })
103        .align_items(AlignItems::Center)
104        .then(config.modifier);
105    if centered {
106        m = m.justify_content(JustifyContent::Center);
107    }
108    Row(m).child((
109        navigation_icon.unwrap_or(Box(Modifier::new().width(16.0).fill_max_height())),
110        Box(Modifier::new()
111            .padding_values(PaddingValues {
112                left: 16.0,
113                right: if centered { 0.0 } else { 0.0 },
114                top: 0.0,
115                bottom: 0.0,
116            })
117            .flex_grow(1.0))
118        .child(with_content_color(
119            config.colors.title_content_color,
120            || title,
121        )),
122        Row(Modifier::new()
123            .align_items(AlignItems::Center)
124            .clip_rounded(20.0))
125        .child(
126            actions
127                .into_iter()
128                .map(|a| {
129                    with_content_color(config.colors.action_icon_content_color, move || a.clone())
130                })
131                .collect::<Vec<_>>(),
132        ),
133    ))
134}
135
136/// M3 Top App Bar (small). Displays a title with optional navigation icon and
137/// trailing action buttons.
138pub fn TopAppBar(
139    title: View,
140    navigation_icon: Option<View>,
141    actions: Vec<View>,
142    config: TopAppBarConfig,
143) -> View {
144    top_app_bar_layout(title, navigation_icon, actions, config, false)
145}
146
147/// M3 Center-Aligned Top App Bar - same as TopAppBar but title is centered.
148pub fn CenterAlignedTopAppBar(
149    title: View,
150    navigation_icon: Option<View>,
151    actions: Vec<View>,
152    config: TopAppBarConfig,
153) -> View {
154    top_app_bar_layout(title, navigation_icon, actions, config, true)
155}
156
157/// Configuration for [`Surface`].
158#[derive(Clone, Debug)]
159pub struct SurfaceConfig {
160    pub modifier: Modifier,
161    pub color: Color,
162    pub content_color: Color,
163    pub shape_radius: f32,
164    pub tonal_elevation: f32,
165    pub shadow_elevation: f32,
166    pub border: Option<(f32, Color)>,
167}
168
169impl Default for SurfaceConfig {
170    fn default() -> Self {
171        Self {
172            modifier: Modifier::new(),
173            color: SurfaceDefaults::color(),
174            content_color: SurfaceDefaults::content_color(),
175            shape_radius: SurfaceDefaults::SHAPE_RADIUS,
176            tonal_elevation: SurfaceDefaults::TONAL_ELEVATION,
177            shadow_elevation: SurfaceDefaults::SHADOW_ELEVATION,
178            border: None,
179        }
180    }
181}
182
183/// M3 Surface - a basic container with shape, color, elevation, and border.
184/// Sets the ContentColor local for children based on the surface color.
185pub fn Surface(config: SurfaceConfig, content: impl FnOnce() -> View) -> View {
186    let mut m = Modifier::new()
187        .background(config.color)
188        .clip_rounded(config.shape_radius)
189        .then(config.modifier);
190    if config.tonal_elevation > 0.0 {
191        m = m.state_elevation(StateElevation {
192            default: config.tonal_elevation,
193            hovered: config.tonal_elevation,
194            pressed: config.tonal_elevation,
195            disabled: 0.0,
196        });
197    }
198    if config.shadow_elevation > 0.0 {
199        m = m.shadow(config.shadow_elevation, 0.0);
200    }
201    if let Some((w, c)) = config.border {
202        m = m.border(w, c, config.shape_radius);
203    }
204    Box(m).color(config.content_color).child(content())
205}
206
207/// Color slots for icon buttons.
208#[derive(Clone, Copy, Debug)]
209pub struct IconButtonColors {
210    pub container_color: Color,
211    pub content_color: Color,
212    pub disabled_container_color: Color,
213    pub disabled_content_color: Color,
214}
215
216impl IconButtonColors {
217    pub fn container(&self, enabled: bool) -> Color {
218        if enabled {
219            self.container_color
220        } else {
221            self.disabled_container_color
222        }
223    }
224    pub fn content(&self, enabled: bool) -> Color {
225        if enabled {
226            self.content_color
227        } else {
228            self.disabled_content_color
229        }
230    }
231}
232
233/// Configuration for [`IconButton`], [`FilledIconButton`], [`FilledTonalIconButton`], and [`OutlinedIconButton`].
234#[derive(Clone, Debug)]
235pub struct IconButtonConfig {
236    pub modifier: Modifier,
237    pub enabled: bool,
238    pub colors: IconButtonColors,
239    pub container_size: Option<f32>,
240}
241
242impl Default for IconButtonConfig {
243    fn default() -> Self {
244        Self {
245            modifier: Modifier::new(),
246            enabled: true,
247            colors: IconButtonColors {
248                container_color: Color::TRANSPARENT,
249                content_color: IconButtonDefaults::content_color(),
250                disabled_container_color: Color::TRANSPARENT,
251                disabled_content_color: IconButtonDefaults::disabled_content_color(),
252            },
253            container_size: None,
254        }
255    }
256}
257
258fn icon_button_render(
259    icon: View,
260    on_click: impl Fn() + 'static,
261    config: &IconButtonConfig,
262    sz: f32,
263    bg: Option<Color>,
264    bdr: Option<(f32, Color)>,
265    state_colors: StateColors,
266) -> View {
267    let is_enabled = config.enabled;
268    let content_color = config.colors.content(is_enabled);
269    let mut m = Modifier::new()
270        .size(sz, sz)
271        .clip_rounded(sz * 0.5)
272        .state_colors(state_colors)
273        .align_items(AlignItems::Center)
274        .justify_content(JustifyContent::Center)
275        .then(config.modifier.clone());
276
277    if let Some(bg_color) = bg {
278        m = m.background(bg_color);
279    }
280    if let Some((w, c)) = bdr {
281        m = m.border(w, c, sz * 0.5);
282    }
283    if is_enabled {
284        m = m.clickable().on_pointer_down(move |_| on_click());
285    }
286
287    Box(m).child(icon)
288}
289
290/// M3 Icon Button - a tappable circular container for an icon.
291pub fn IconButton(icon: View, on_click: impl Fn() + 'static, config: IconButtonConfig) -> View {
292    let th = theme();
293    let sz = config
294        .container_size
295        .unwrap_or(IconButtonDefaults::CONTAINER_SIZE);
296    icon_button_render(
297        icon,
298        on_click,
299        &config,
300        sz,
301        None,
302        None,
303        StateColors {
304            default: Color::TRANSPARENT,
305            hovered: th.on_surface.with_alpha_f32(0.08),
306            pressed: th.on_surface.with_alpha_f32(0.12),
307            disabled: Color::TRANSPARENT,
308        },
309    )
310}
311
312/// M3 Filled Icon Button - icon button with a filled container background.
313pub fn FilledIconButton(
314    icon: View,
315    on_click: impl Fn() + 'static,
316    config: IconButtonConfig,
317) -> View {
318    let th = theme();
319    let is_enabled = config.enabled;
320    let sz = config
321        .container_size
322        .unwrap_or(IconButtonDefaults::FILLED_CONTAINER_SIZE);
323    let bg = config.colors.container(is_enabled);
324    let content_color = config.colors.content(is_enabled);
325    icon_button_render(
326        icon,
327        on_click,
328        &config,
329        sz,
330        Some(bg),
331        None,
332        StateColors {
333            default: Color::TRANSPARENT,
334            hovered: content_color.with_alpha_f32(0.08),
335            pressed: content_color.with_alpha_f32(0.12),
336            disabled: th.on_surface.with_alpha_f32(0.12),
337        },
338    )
339}
340
341/// M3 Filled Tonal Icon Button - icon button with a secondary container background.
342pub fn FilledTonalIconButton(
343    icon: View,
344    on_click: impl Fn() + 'static,
345    config: IconButtonConfig,
346) -> View {
347    let th = theme();
348    let is_enabled = config.enabled;
349    let sz = config
350        .container_size
351        .unwrap_or(IconButtonDefaults::FILLED_CONTAINER_SIZE);
352    let bg = config.colors.container(is_enabled);
353    let content_color = config.colors.content(is_enabled);
354    icon_button_render(
355        icon,
356        on_click,
357        &config,
358        sz,
359        Some(bg),
360        None,
361        StateColors {
362            default: Color::TRANSPARENT,
363            hovered: content_color.with_alpha_f32(0.08),
364            pressed: content_color.with_alpha_f32(0.12),
365            disabled: th.on_surface.with_alpha_f32(0.12),
366        },
367    )
368}
369
370/// M3 Outlined Icon Button - icon button with a transparent background and border.
371pub fn OutlinedIconButton(
372    icon: View,
373    on_click: impl Fn() + 'static,
374    config: IconButtonConfig,
375) -> View {
376    let th = theme();
377    let sz = config
378        .container_size
379        .unwrap_or(IconButtonDefaults::CONTAINER_SIZE);
380    let border_color = if config.enabled {
381        th.outline
382    } else {
383        th.on_surface.with_alpha_f32(0.12)
384    };
385    icon_button_render(
386        icon,
387        on_click,
388        &config,
389        sz,
390        None,
391        Some((1.0, border_color)),
392        StateColors {
393            default: Color::TRANSPARENT,
394            hovered: th.on_surface.with_alpha_f32(0.08),
395            pressed: th.on_surface.with_alpha_f32(0.12),
396            disabled: Color::TRANSPARENT,
397        },
398    )
399}
400
401/// Color slots for buttons (matching Compose Material3 `ButtonColors`).
402#[derive(Clone, Copy, Debug)]
403pub struct ButtonColors {
404    pub container_color: Color,
405    pub content_color: Color,
406    pub disabled_container_color: Color,
407    pub disabled_content_color: Color,
408}
409
410impl ButtonColors {
411    pub fn container(&self, enabled: bool) -> Color {
412        if enabled {
413            self.container_color
414        } else {
415            self.disabled_container_color
416        }
417    }
418    pub fn content(&self, enabled: bool) -> Color {
419        if enabled {
420            self.content_color
421        } else {
422            self.disabled_content_color
423        }
424    }
425}
426
427/// Elevation levels for buttons (matching Compose Material3 `ButtonElevation`).
428#[derive(Clone, Copy, Debug)]
429pub struct ButtonElevation {
430    pub default: f32,
431    pub pressed: f32,
432    pub focused: f32,
433    pub hovered: f32,
434    pub disabled: f32,
435}
436
437/// Configuration for button components.
438#[derive(Clone, Debug)]
439pub struct ButtonConfig {
440    pub modifier: Modifier,
441    pub enabled: bool,
442    pub content_color: Option<Color>,
443    pub container_color: Option<Color>,
444    pub state_colors: StateColors,
445    pub state_elevation: Option<StateElevation>,
446    pub border: Option<(f32, Color, f32)>,
447    pub shape_radius: f32,
448    pub content_padding: Option<PaddingValues>,
449    pub height: f32,
450    pub colors: Option<ButtonColors>,
451    pub elevation: Option<ButtonElevation>,
452}
453
454impl Default for ButtonConfig {
455    fn default() -> Self {
456        Self {
457            modifier: Modifier::new(),
458            enabled: true,
459            content_color: None,
460            container_color: None,
461            state_colors: ButtonDefaults::state_colors_default(),
462            state_elevation: None,
463            border: None,
464            shape_radius: ButtonDefaults::SHAPE_RADIUS,
465            content_padding: None,
466            height: ButtonDefaults::HEIGHT,
467            colors: None,
468            elevation: None,
469        }
470    }
471}
472
473/// Resolve effective button colors from config, given the variant's default colors.
474/// When `config.colors` is set, it takes priority over individual fields.
475fn resolve_button_colors(
476    config: &ButtonConfig,
477    def: ButtonColors,
478) -> (Color, Option<Color>, StateColors, Option<StateElevation>) {
479    if let Some(colors) = &config.colors {
480        let bg = if config.enabled {
481            colors.container_color
482        } else {
483            colors.disabled_container_color
484        };
485        let cc = if config.enabled {
486            colors.content_color
487        } else {
488            colors.disabled_content_color
489        };
490        let sc = StateColors {
491            default: Color::TRANSPARENT,
492            hovered: colors.content_color.with_alpha_f32(0.08),
493            pressed: colors.content_color.with_alpha_f32(0.12),
494            disabled: Color::TRANSPARENT,
495        };
496        let se = config.elevation.map(|e| StateElevation {
497            default: e.default,
498            hovered: e.hovered,
499            pressed: e.pressed,
500            disabled: e.disabled,
501        });
502        (cc, Some(bg), sc, se)
503    } else {
504        let cc = config.content_color.unwrap_or(def.content_color);
505        let bg = Some(config.container_color.unwrap_or(def.container_color));
506        let sc = if config.enabled {
507            config.state_colors
508        } else {
509            StateColors {
510                default: Color::TRANSPARENT,
511                hovered: Color::TRANSPARENT,
512                pressed: Color::TRANSPARENT,
513                disabled: config.state_colors.disabled,
514            }
515        };
516        let se = config.state_elevation;
517        (cc, bg, sc, se)
518    }
519}
520
521fn button_impl(
522    outer_modifier: Modifier,
523    on_click: impl Fn() + 'static,
524    content: impl FnOnce() -> View,
525    content_color: Color,
526    container_color: Option<Color>,
527    state_colors: StateColors,
528    state_elevation: Option<StateElevation>,
529    border: Option<(f32, Color, f32)>,
530    padding_left: f32,
531    padding_right: f32,
532    height: f32,
533    shape_radius: f32,
534    enabled: bool,
535) -> View {
536    let mut m = Modifier::new().height(height).min_width(48.0);
537    if let Some(bg) = container_color {
538        m = m.background(bg);
539    }
540    m = m.state_colors(if enabled {
541        state_colors
542    } else {
543        StateColors {
544            default: Color::TRANSPARENT,
545            hovered: Color::TRANSPARENT,
546            pressed: Color::TRANSPARENT,
547            disabled: state_colors.disabled,
548        }
549    });
550    if let Some(se) = state_elevation {
551        m = m.state_elevation(se);
552    }
553    if let Some((w, c, r)) = border {
554        m = m.border(w, c, r);
555    }
556    m = m
557        .clip_rounded(shape_radius)
558        .padding_values(PaddingValues {
559            left: padding_left,
560            right: padding_right,
561            top: 0.0,
562            bottom: 0.0,
563        })
564        .align_items(AlignItems::Center)
565        .justify_content(JustifyContent::Center);
566
567    // Interaction source + ripple indication (matching Compose's clickable + indication wiring)
568    let source = remember(MutableInteractionSource::new);
569    m = m.interaction_source(&*source);
570    m = m.indication(ripple(RippleConfig {
571        color: Some(content_color),
572        bounded: true,
573        ..Default::default()
574    }));
575
576    if enabled {
577        m = m.clickable().on_pointer_down(move |_| on_click());
578    }
579    m = m.then(outer_modifier);
580    let effective = if enabled {
581        content_color
582    } else {
583        content_color.with_alpha_f32(0.38)
584    };
585    let content = with_content_color(effective, content);
586    Box(m).child(content)
587}
588
589/// M3 Button - prominent action button with primary color fill.
590/// (Equivalent to Compose Material3's `Button`.)
591pub fn Button(
592    modifier: Modifier,
593    on_click: impl Fn() + 'static,
594    config: ButtonConfig,
595    content: impl FnOnce() -> View,
596) -> View {
597    let def = ButtonColors {
598        container_color: ButtonDefaults::container_color(),
599        content_color: ButtonDefaults::content_color(),
600        disabled_container_color: ButtonDefaults::container_color()
601            .with_alpha_f32(0.12)
602            .composite_over(theme().surface_container_low),
603        disabled_content_color: ButtonDefaults::content_color().with_alpha_f32(0.38),
604    };
605    let (cc, bg, sc, se) = resolve_button_colors(&config, def);
606    let pad = config.content_padding.unwrap_or(PaddingValues {
607        left: 24.0,
608        right: 24.0,
609        top: 0.0,
610        bottom: 0.0,
611    });
612    button_impl(
613        modifier.then(config.modifier),
614        on_click,
615        content,
616        cc,
617        bg,
618        sc,
619        se.or(Some(ButtonDefaults::state_elevation_default())),
620        config.border,
621        pad.left,
622        pad.right,
623        config.height,
624        config.shape_radius,
625        config.enabled,
626    )
627}
628
629/// M3 Filled Tonal Button - uses secondary container colors.
630pub fn FilledTonalButton(
631    modifier: Modifier,
632    on_click: impl Fn() + 'static,
633    config: ButtonConfig,
634    content: impl FnOnce() -> View,
635) -> View {
636    let th = theme();
637    let def = ButtonColors {
638        container_color: ButtonDefaults::tonal_container_color(),
639        content_color: ButtonDefaults::tonal_content_color(),
640        disabled_container_color: th
641            .on_surface
642            .with_alpha_f32(0.12)
643            .composite_over(th.surface_container_low),
644        disabled_content_color: th.on_surface.with_alpha_f32(0.38),
645    };
646    let (cc, bg, sc, se) = resolve_button_colors(&config, def);
647    let pad = config.content_padding.unwrap_or(PaddingValues {
648        left: 24.0,
649        right: 24.0,
650        top: 0.0,
651        bottom: 0.0,
652    });
653    button_impl(
654        modifier.then(config.modifier),
655        on_click,
656        content,
657        cc,
658        bg,
659        sc,
660        se.or(Some(ButtonDefaults::state_elevation_default())),
661        config.border,
662        pad.left,
663        pad.right,
664        config.height,
665        config.shape_radius,
666        config.enabled,
667    )
668}
669
670/// M3 Outlined Button - button with an outline border and no fill.
671pub fn OutlinedButton(
672    modifier: Modifier,
673    on_click: impl Fn() + 'static,
674    config: ButtonConfig,
675    content: impl FnOnce() -> View,
676) -> View {
677    let th = theme();
678    let def = ButtonColors {
679        container_color: Color::TRANSPARENT,
680        content_color: ButtonDefaults::outlined_content_color(),
681        disabled_container_color: Color::TRANSPARENT,
682        disabled_content_color: th.on_surface.with_alpha_f32(0.38),
683    };
684    let (cc, bg, sc, se) = resolve_button_colors(&config, def);
685    let border = config
686        .border
687        .unwrap_or((1.0, ButtonDefaults::outlined_border_color(), 20.0));
688    let pad = config.content_padding.unwrap_or(PaddingValues {
689        left: 24.0,
690        right: 24.0,
691        top: 0.0,
692        bottom: 0.0,
693    });
694    button_impl(
695        modifier.then(config.modifier),
696        on_click,
697        content,
698        cc,
699        bg,
700        sc,
701        se,
702        Some(border),
703        pad.left,
704        pad.right,
705        config.height,
706        config.shape_radius,
707        config.enabled,
708    )
709}
710
711/// M3 Text Button - a low-emphasis button.
712pub fn TextButton(
713    modifier: Modifier,
714    on_click: impl Fn() + 'static,
715    config: ButtonConfig,
716    content: impl FnOnce() -> View,
717) -> View {
718    let th = theme();
719    let def = ButtonColors {
720        container_color: Color::TRANSPARENT,
721        content_color: ButtonDefaults::text_content_color(),
722        disabled_container_color: Color::TRANSPARENT,
723        disabled_content_color: th.on_surface.with_alpha_f32(0.38),
724    };
725    let (cc, bg, sc, se) = resolve_button_colors(&config, def);
726    let pad = config.content_padding.unwrap_or(PaddingValues {
727        left: 12.0,
728        right: 12.0,
729        top: 0.0,
730        bottom: 0.0,
731    });
732    button_impl(
733        modifier.then(config.modifier),
734        on_click,
735        content,
736        cc,
737        bg,
738        sc,
739        se,
740        None,
741        pad.left,
742        pad.right,
743        config.height,
744        config.shape_radius,
745        config.enabled,
746    )
747}
748
749/// M3 Elevated Button - uses `surface_container_low` background with elevation.
750pub fn ElevatedButton(
751    modifier: Modifier,
752    on_click: impl Fn() + 'static,
753    config: ButtonConfig,
754    content: impl FnOnce() -> View,
755) -> View {
756    let th = theme();
757    let def = ButtonColors {
758        container_color: ButtonDefaults::elevated_container_color(),
759        content_color: ButtonDefaults::elevated_content_color(),
760        disabled_container_color: th.on_surface.with_alpha_f32(0.04),
761        disabled_content_color: th.on_surface.with_alpha_f32(0.38),
762    };
763    let (cc, bg, sc, se) = resolve_button_colors(&config, def);
764    let pad = config.content_padding.unwrap_or(PaddingValues {
765        left: 24.0,
766        right: 24.0,
767        top: 0.0,
768        bottom: 0.0,
769    });
770    button_impl(
771        modifier.then(config.modifier),
772        on_click,
773        content,
774        cc,
775        bg,
776        sc,
777        se.or(Some(ButtonDefaults::elevated_state_elevation())),
778        config.border,
779        pad.left,
780        pad.right,
781        config.height,
782        config.shape_radius,
783        config.enabled,
784    )
785}
786
787/// Configuration for toggle button components.
788#[derive(Clone, Debug)]
789pub struct ToggleButtonConfig {
790    pub modifier: Modifier,
791    pub enabled: bool,
792    pub container_color: Option<Color>,
793    pub content_color: Option<Color>,
794    pub checked_container_color: Option<Color>,
795    pub checked_content_color: Option<Color>,
796    pub state_colors: StateColors,
797    pub state_elevation: Option<StateElevation>,
798    pub border: Option<(f32, Color, f32)>,
799    pub shape_radius: f32,
800    pub height: f32,
801}
802
803impl Default for ToggleButtonConfig {
804    fn default() -> Self {
805        Self {
806            modifier: Modifier::new(),
807            enabled: true,
808            container_color: None,
809            content_color: None,
810            checked_container_color: None,
811            checked_content_color: None,
812            state_colors: ToggleButtonDefaults::state_colors_default(),
813            state_elevation: None,
814            border: None,
815            shape_radius: ToggleButtonDefaults::SHAPE_RADIUS,
816            height: ToggleButtonDefaults::HEIGHT,
817        }
818    }
819}
820
821fn toggle_button_impl(
822    checked: bool,
823    on_checked_change: impl Fn(bool) + 'static,
824    content: impl FnOnce(bool) -> View,
825    content_color: Color,
826    container_color: Option<Color>,
827    checked_container_color: Option<Color>,
828    checked_content_color: Option<Color>,
829    state_colors: StateColors,
830    state_elevation: StateElevation,
831    border: Option<(f32, Color, f32)>,
832    pad_left: f32,
833    pad_right: f32,
834    height: f32,
835    shape_radius: f32,
836    enabled: bool,
837) -> View {
838    let th = theme();
839    let bg = if checked {
840        checked_container_color.unwrap_or(th.primary)
841    } else {
842        container_color.unwrap_or(Color::TRANSPARENT)
843    };
844    let fg = if checked {
845        checked_content_color.unwrap_or(th.on_primary)
846    } else {
847        content_color
848    };
849    let mut m = Modifier::new()
850        .height(height)
851        .padding_values(PaddingValues {
852            left: pad_left,
853            right: pad_right,
854            top: 0.0,
855            bottom: 0.0,
856        })
857        .background(bg)
858        .clip_rounded(shape_radius)
859        .state_colors(state_colors)
860        .state_elevation(state_elevation);
861    if let Some((w, c, r)) = border {
862        m = m.border(w, c, r);
863    }
864    if enabled {
865        m = m.clickable().on_pointer_down({
866            let cb = on_checked_change;
867            move |_| cb(!checked)
868        });
869    } else {
870        m = m.alpha(0.38);
871    }
872    with_content_color(fg, || Box(m).child(content(checked)))
873}
874
875/// M3 Toggle Button - a button that toggles between checked/unchecked states.
876pub fn ToggleButton(
877    checked: bool,
878    on_checked_change: impl Fn(bool) + 'static,
879    config: ToggleButtonConfig,
880    content: impl FnOnce(bool) -> View,
881) -> View {
882    let cc = config
883        .content_color
884        .unwrap_or_else(ToggleButtonDefaults::content_color);
885    let checked_cc = config
886        .checked_content_color
887        .unwrap_or_else(ToggleButtonDefaults::checked_content_color);
888    let checked_bg = config
889        .checked_container_color
890        .unwrap_or_else(ToggleButtonDefaults::checked_container_color);
891    let se = config
892        .state_elevation
893        .unwrap_or_else(ToggleButtonDefaults::state_elevation_default);
894    toggle_button_impl(
895        checked,
896        on_checked_change,
897        content,
898        cc,
899        None,
900        Some(checked_bg),
901        Some(checked_cc),
902        config.state_colors,
903        se,
904        config.border,
905        ToggleButtonDefaults::HORIZONTAL_PADDING,
906        ToggleButtonDefaults::HORIZONTAL_PADDING,
907        config.height,
908        config.shape_radius,
909        config.enabled,
910    )
911}
912
913/// M3 Tonal Toggle Button - uses secondary container colors.
914pub fn TonalToggleButton(
915    checked: bool,
916    on_checked_change: impl Fn(bool) + 'static,
917    config: ToggleButtonConfig,
918    content: impl FnOnce(bool) -> View,
919) -> View {
920    let cc = config
921        .content_color
922        .unwrap_or_else(ToggleButtonDefaults::tonal_content_color);
923    let checked_cc = config
924        .checked_content_color
925        .unwrap_or_else(ToggleButtonDefaults::tonal_checked_content_color);
926    let checked_bg = config
927        .checked_container_color
928        .unwrap_or_else(ToggleButtonDefaults::tonal_checked_container_color);
929    let se = config
930        .state_elevation
931        .unwrap_or_else(ToggleButtonDefaults::state_elevation_default);
932    toggle_button_impl(
933        checked,
934        on_checked_change,
935        content,
936        cc,
937        None,
938        Some(checked_bg),
939        Some(checked_cc),
940        config.state_colors,
941        se,
942        config.border,
943        ToggleButtonDefaults::HORIZONTAL_PADDING,
944        ToggleButtonDefaults::HORIZONTAL_PADDING,
945        config.height,
946        config.shape_radius,
947        config.enabled,
948    )
949}
950
951/// M3 Outlined Toggle Button - outlined button that toggles between states.
952pub fn OutlinedToggleButton(
953    checked: bool,
954    on_checked_change: impl Fn(bool) + 'static,
955    config: ToggleButtonConfig,
956    content: impl FnOnce(bool) -> View,
957) -> View {
958    let cc = config
959        .content_color
960        .unwrap_or_else(ToggleButtonDefaults::outlined_content_color);
961    let checked_cc = config
962        .checked_content_color
963        .unwrap_or_else(ToggleButtonDefaults::outlined_checked_content_color);
964    let checked_bg = config
965        .checked_container_color
966        .unwrap_or_else(ToggleButtonDefaults::outlined_checked_container_color);
967    let se = config
968        .state_elevation
969        .unwrap_or_else(ToggleButtonDefaults::state_elevation_default);
970    let border = if !checked {
971        Some(config.border.unwrap_or((
972            1.0,
973            ToggleButtonDefaults::outlined_border_color(),
974            config.shape_radius,
975        )))
976    } else {
977        config.border
978    };
979    toggle_button_impl(
980        checked,
981        on_checked_change,
982        content,
983        cc,
984        None,
985        Some(checked_bg),
986        Some(checked_cc),
987        config.state_colors,
988        se,
989        border,
990        ToggleButtonDefaults::HORIZONTAL_PADDING,
991        ToggleButtonDefaults::HORIZONTAL_PADDING,
992        config.height,
993        config.shape_radius,
994        config.enabled,
995    )
996}
997
998/// M3 Elevated Toggle Button - elevated button that toggles between states.
999pub fn ElevatedToggleButton(
1000    checked: bool,
1001    on_checked_change: impl Fn(bool) + 'static,
1002    config: ToggleButtonConfig,
1003    content: impl FnOnce(bool) -> View,
1004) -> View {
1005    let cc = config
1006        .content_color
1007        .unwrap_or_else(ToggleButtonDefaults::elevated_content_color);
1008    let checked_cc = config
1009        .checked_content_color
1010        .unwrap_or_else(ToggleButtonDefaults::elevated_checked_content_color);
1011    let checked_bg = config
1012        .checked_container_color
1013        .unwrap_or_else(ToggleButtonDefaults::elevated_checked_container_color);
1014    let se = config
1015        .state_elevation
1016        .unwrap_or_else(ToggleButtonDefaults::elevated_state_elevation);
1017    toggle_button_impl(
1018        checked,
1019        on_checked_change,
1020        content,
1021        cc,
1022        None,
1023        Some(checked_bg),
1024        Some(checked_cc),
1025        config.state_colors,
1026        se,
1027        config.border,
1028        ToggleButtonDefaults::HORIZONTAL_PADDING,
1029        ToggleButtonDefaults::HORIZONTAL_PADDING,
1030        config.height,
1031        config.shape_radius,
1032        config.enabled,
1033    )
1034}
1035
1036/// Configuration for FAB components.
1037#[derive(Clone, Debug)]
1038pub struct FABConfig {
1039    pub modifier: Modifier,
1040    pub enabled: bool,
1041    pub container_color: Color,
1042    pub content_color: Color,
1043    pub state_elevation: StateElevation,
1044    pub shape_radius: f32,
1045    pub size: f32,
1046}
1047
1048impl Default for FABConfig {
1049    fn default() -> Self {
1050        Self {
1051            modifier: Modifier::new(),
1052            enabled: true,
1053            container_color: FABDefaults::container_color(),
1054            content_color: FABDefaults::content_color(),
1055            state_elevation: FABDefaults::state_elevation(),
1056            shape_radius: FABDefaults::SHAPE_RADIUS,
1057            size: FABDefaults::SIZE,
1058        }
1059    }
1060}
1061
1062fn fab_impl(
1063    icon: View,
1064    on_click: impl Fn() + 'static,
1065    size: f32,
1066    shape_r: f32,
1067    config: FABConfig,
1068) -> View {
1069    let th = theme();
1070    let is_enabled = config.enabled;
1071    let bg = if is_enabled {
1072        config.container_color
1073    } else {
1074        th.on_surface
1075            .with_alpha_f32(0.12)
1076            .composite_over(th.surface_container_low)
1077    };
1078    let content_color = if is_enabled {
1079        config.content_color
1080    } else {
1081        th.on_surface.with_alpha_f32(0.38)
1082    };
1083
1084    let mut m = Modifier::new()
1085        .size(size, size)
1086        .background(bg)
1087        .state_colors(StateColors {
1088            default: Color::TRANSPARENT,
1089            hovered: config.content_color.with_alpha_f32(0.08),
1090            pressed: config.content_color.with_alpha_f32(0.12),
1091            disabled: th.on_surface.with_alpha_f32(0.12),
1092        })
1093        .state_elevation(config.state_elevation)
1094        .clip_rounded(shape_r)
1095        .align_items(AlignItems::Center)
1096        .justify_content(JustifyContent::Center)
1097        .then(config.modifier);
1098
1099    if is_enabled {
1100        m = m.clickable().on_pointer_down(move |_| on_click());
1101    }
1102
1103    Box(m).child(icon)
1104}
1105
1106/// M3 Floating Action Button (regular, 56dp).
1107pub fn FAB(icon: View, on_click: impl Fn() + 'static, config: FABConfig) -> View {
1108    fab_impl(
1109        icon,
1110        on_click,
1111        FABDefaults::SIZE,
1112        FABDefaults::SHAPE_RADIUS,
1113        config,
1114    )
1115}
1116
1117/// M3 Small FAB (40dp).
1118pub fn SmallFAB(icon: View, on_click: impl Fn() + 'static, config: FABConfig) -> View {
1119    fab_impl(
1120        icon,
1121        on_click,
1122        FABDefaults::SMALL_SIZE,
1123        FABDefaults::SMALL_SHAPE_RADIUS,
1124        config,
1125    )
1126}
1127
1128/// M3 Large FAB (96dp).
1129pub fn LargeFAB(icon: View, on_click: impl Fn() + 'static, config: FABConfig) -> View {
1130    fab_impl(
1131        icon,
1132        on_click,
1133        FABDefaults::LARGE_SIZE,
1134        FABDefaults::LARGE_SHAPE_RADIUS,
1135        config,
1136    )
1137}
1138
1139/// M3 Extended FAB - FAB with icon + label.
1140pub fn ExtendedFAB(
1141    icon: Option<View>,
1142    label: impl Into<String>,
1143    on_click: impl Fn() + 'static,
1144    config: FABConfig,
1145) -> View {
1146    let th = theme();
1147    let has_icon = icon.is_some();
1148    let is_enabled = config.enabled;
1149    let bg = if is_enabled {
1150        config.container_color
1151    } else {
1152        th.on_surface
1153            .with_alpha_f32(0.12)
1154            .composite_over(th.surface_container_low)
1155    };
1156    let content_color = if is_enabled {
1157        config.content_color
1158    } else {
1159        th.on_surface.with_alpha_f32(0.38)
1160    };
1161
1162    let mut m = Modifier::new()
1163        .height(56.0)
1164        .min_width(80.0)
1165        .background(bg)
1166        .state_colors(StateColors {
1167            default: Color::TRANSPARENT,
1168            hovered: config.content_color.with_alpha_f32(0.08),
1169            pressed: config.content_color.with_alpha_f32(0.12),
1170            disabled: theme().on_surface.with_alpha_f32(0.12),
1171        })
1172        .state_elevation(config.state_elevation)
1173        .clip_rounded(FABDefaults::SHAPE_RADIUS)
1174        .padding_values(PaddingValues {
1175            left: 16.0,
1176            right: 20.0,
1177            top: 0.0,
1178            bottom: 0.0,
1179        })
1180        .align_items(AlignItems::Center);
1181
1182    if is_enabled {
1183        m = m.clickable().on_pointer_down(move |_| on_click());
1184    }
1185    m = m.then(config.modifier);
1186    Row(m).child((
1187        icon.unwrap_or(Box(Modifier::new())),
1188        Box(Modifier::new()
1189            .width(if has_icon { 12.0 } else { 0.0 })
1190            .fill_max_height()),
1191        Text(label)
1192            .color(content_color)
1193            .size(th.typography.label_large)
1194            .single_line(),
1195    ))
1196}
1197
1198/// Configuration for divider components.
1199#[derive(Clone, Debug)]
1200pub struct DividerConfig {
1201    pub modifier: Modifier,
1202    pub thickness: f32,
1203    pub color: Color,
1204}
1205
1206impl Default for DividerConfig {
1207    fn default() -> Self {
1208        Self {
1209            modifier: Modifier::new(),
1210            thickness: DividerDefaults::THICKNESS,
1211            color: DividerDefaults::color(),
1212        }
1213    }
1214}
1215
1216/// M3 Horizontal Divider - a thin 1dp line.
1217/// (Equivalent to Compose Material3's `HorizontalDivider`.)
1218pub fn HorizontalDivider(config: DividerConfig) -> View {
1219    Box(Modifier::new()
1220        .min_width(200.0)
1221        .height(config.thickness)
1222        .background(config.color)
1223        .then(config.modifier))
1224}
1225
1226#[deprecated(since = "0.19.5", note = "renamed to HorizontalDivider")]
1227pub fn Divider(config: DividerConfig) -> View {
1228    HorizontalDivider(config)
1229}
1230
1231/// M3 Vertical Divider - a thin 1dp vertical line.
1232pub fn VerticalDivider(config: DividerConfig) -> View {
1233    Box(Modifier::new()
1234        .width(config.thickness)
1235        .fill_max_height()
1236        .background(config.color)
1237        .then(config.modifier))
1238}
1239
1240/// Configuration for [`Badge`].
1241#[derive(Clone, Debug)]
1242pub struct BadgeConfig {
1243    pub modifier: Modifier,
1244    pub container_color: Color,
1245    pub content_color: Color,
1246}
1247
1248impl Default for BadgeConfig {
1249    fn default() -> Self {
1250        Self {
1251            modifier: Modifier::new(),
1252            container_color: BadgeDefaults::container_color(),
1253            content_color: BadgeDefaults::content_color(),
1254        }
1255    }
1256}
1257
1258/// M3 Badge - a small notification indicator. If `content` is `None`, shows a
1259/// small 6dp dot; otherwise shows the content inside a 16dp pill.
1260pub fn Badge(content: Option<View>, config: BadgeConfig) -> View {
1261    match content {
1262        None => Box(Modifier::new()
1263            .size(BadgeDefaults::DOT_SIZE, BadgeDefaults::DOT_SIZE)
1264            .background(config.container_color)
1265            .clip_rounded(BadgeDefaults::DOT_SIZE * 0.5)
1266            .then(config.modifier)),
1267        Some(view) => Box(Modifier::new()
1268            .min_width(BadgeDefaults::LABEL_MIN_WIDTH)
1269            .height(BadgeDefaults::LABEL_HEIGHT)
1270            .background(config.container_color)
1271            .clip_rounded(BadgeDefaults::LABEL_HEIGHT * 0.5)
1272            .padding_values(PaddingValues {
1273                left: 4.0,
1274                right: 4.0,
1275                top: 0.0,
1276                bottom: 0.0,
1277            })
1278            .align_items(AlignItems::Center)
1279            .justify_content(JustifyContent::Center)
1280            .then(config.modifier))
1281        .child(view),
1282    }
1283}
1284
1285/// Configuration for [`BadgedBox`].
1286#[derive(Clone, Debug)]
1287pub struct BadgedBoxConfig {
1288    pub modifier: Modifier,
1289    /// Horizontal offset for the badge when it's a small dot.
1290    pub dot_offset_x: f32,
1291    /// Vertical offset for the badge when it's a small dot.
1292    pub dot_offset_y: f32,
1293    /// Horizontal offset for the badge when it has content.
1294    pub content_offset_x: f32,
1295    /// Vertical offset for the badge when it has content.
1296    pub content_offset_y: f32,
1297}
1298
1299impl Default for BadgedBoxConfig {
1300    fn default() -> Self {
1301        Self {
1302            modifier: Modifier::new(),
1303            dot_offset_x: BadgeDefaults::DOT_OFFSET_X,
1304            dot_offset_y: BadgeDefaults::DOT_OFFSET_Y,
1305            content_offset_x: BadgeDefaults::CONTENT_OFFSET_X,
1306            content_offset_y: BadgeDefaults::CONTENT_OFFSET_Y,
1307        }
1308    }
1309}
1310
1311/// M3 BadgedBox - wraps `content` and shows a `badge` anchored to the top-end corner.
1312/// The badge is positioned at the top-end corner of the content.
1313pub fn BadgedBox(badge: View, content: View, config: BadgedBoxConfig) -> View {
1314    Stack(Modifier::new()).child((
1315        content,
1316        Box(Modifier::new().absolute().offset(
1317            None,
1318            Some(config.dot_offset_y),
1319            Some(config.dot_offset_x),
1320            None,
1321        ))
1322        .child(badge),
1323    ))
1324}
1325
1326/// Colors for [`ListItem`] — matches Compose Material3 `ListItemColors` with
1327/// 4 state groups (default, disabled, selected, dragged) × 6 slots each.
1328#[derive(Clone, Debug)]
1329pub struct ListItemColors {
1330    pub container_color: Color,
1331    pub headline_color: Color,
1332    pub supporting_color: Color,
1333    pub overline_color: Color,
1334    pub leading_icon_color: Color,
1335    pub trailing_icon_color: Color,
1336
1337    pub disabled_container_color: Color,
1338    pub disabled_headline_color: Color,
1339    pub disabled_supporting_color: Color,
1340    pub disabled_overline_color: Color,
1341    pub disabled_leading_icon_color: Color,
1342    pub disabled_trailing_icon_color: Color,
1343
1344    pub selected_container_color: Color,
1345    pub selected_headline_color: Color,
1346    pub selected_supporting_color: Color,
1347    pub selected_overline_color: Color,
1348    pub selected_leading_icon_color: Color,
1349    pub selected_trailing_icon_color: Color,
1350
1351    pub dragged_container_color: Color,
1352    pub dragged_headline_color: Color,
1353    pub dragged_supporting_color: Color,
1354    pub dragged_overline_color: Color,
1355    pub dragged_leading_icon_color: Color,
1356    pub dragged_trailing_icon_color: Color,
1357}
1358
1359impl ListItemColors {
1360    pub fn container(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1361        if !enabled {
1362            self.disabled_container_color
1363        } else if dragged {
1364            self.dragged_container_color
1365        } else if selected {
1366            self.selected_container_color
1367        } else {
1368            self.container_color
1369        }
1370    }
1371    pub fn headline(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1372        if !enabled {
1373            self.disabled_headline_color
1374        } else if dragged {
1375            self.dragged_headline_color
1376        } else if selected {
1377            self.selected_headline_color
1378        } else {
1379            self.headline_color
1380        }
1381    }
1382    pub fn supporting(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1383        if !enabled {
1384            self.disabled_supporting_color
1385        } else if dragged {
1386            self.dragged_supporting_color
1387        } else if selected {
1388            self.selected_supporting_color
1389        } else {
1390            self.supporting_color
1391        }
1392    }
1393    pub fn overline(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1394        if !enabled {
1395            self.disabled_overline_color
1396        } else if dragged {
1397            self.dragged_overline_color
1398        } else if selected {
1399            self.selected_overline_color
1400        } else {
1401            self.overline_color
1402        }
1403    }
1404    pub fn leading_icon(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1405        if !enabled {
1406            self.disabled_leading_icon_color
1407        } else if dragged {
1408            self.dragged_leading_icon_color
1409        } else if selected {
1410            self.selected_leading_icon_color
1411        } else {
1412            self.leading_icon_color
1413        }
1414    }
1415    pub fn trailing_icon(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1416        if !enabled {
1417            self.disabled_trailing_icon_color
1418        } else if dragged {
1419            self.dragged_trailing_icon_color
1420        } else if selected {
1421            self.selected_trailing_icon_color
1422        } else {
1423            self.trailing_icon_color
1424        }
1425    }
1426}
1427
1428impl Default for ListItemColors {
1429    fn default() -> Self {
1430        Self {
1431            container_color: Color::TRANSPARENT,
1432            headline_color: ListItemDefaults::headline_color(),
1433            supporting_color: ListItemDefaults::supporting_color(),
1434            overline_color: ListItemDefaults::overline_color(),
1435            leading_icon_color: ListItemDefaults::leading_icon_color(),
1436            trailing_icon_color: ListItemDefaults::trailing_icon_color(),
1437            disabled_container_color: ListItemDefaults::disabled_container_color(),
1438            disabled_headline_color: ListItemDefaults::disabled_headline_color(),
1439            disabled_supporting_color: ListItemDefaults::disabled_supporting_color(),
1440            disabled_overline_color: ListItemDefaults::disabled_overline_color(),
1441            disabled_leading_icon_color: ListItemDefaults::disabled_leading_icon_color(),
1442            disabled_trailing_icon_color: ListItemDefaults::disabled_trailing_icon_color(),
1443            selected_container_color: ListItemDefaults::selected_container_color(),
1444            selected_headline_color: ListItemDefaults::selected_headline_color(),
1445            selected_supporting_color: ListItemDefaults::selected_supporting_color(),
1446            selected_overline_color: ListItemDefaults::selected_overline_color(),
1447            selected_leading_icon_color: ListItemDefaults::selected_leading_icon_color(),
1448            selected_trailing_icon_color: ListItemDefaults::selected_trailing_icon_color(),
1449            dragged_container_color: ListItemDefaults::dragged_container_color(),
1450            dragged_headline_color: ListItemDefaults::dragged_headline_color(),
1451            dragged_supporting_color: ListItemDefaults::dragged_supporting_color(),
1452            dragged_overline_color: ListItemDefaults::dragged_overline_color(),
1453            dragged_leading_icon_color: ListItemDefaults::dragged_leading_icon_color(),
1454            dragged_trailing_icon_color: ListItemDefaults::dragged_trailing_icon_color(),
1455        }
1456    }
1457}
1458
1459/// Configuration for [`ListItem`].
1460#[derive(Clone, Debug)]
1461pub struct ListItemConfig {
1462    pub modifier: Modifier,
1463    /// When false, renders disabled colors and suppresses clicks.
1464    pub enabled: bool,
1465    pub colors: ListItemColors,
1466    pub tonal_elevation: f32,
1467    pub shadow_elevation: f32,
1468    pub shape_radius: f32,
1469    pub horizontal_padding: f32,
1470    pub trailing_padding: f32,
1471    pub one_line_height: f32,
1472    pub two_line_height: f32,
1473    pub three_line_height: f32,
1474}
1475
1476impl Default for ListItemConfig {
1477    fn default() -> Self {
1478        Self {
1479            modifier: Modifier::new(),
1480            enabled: true,
1481            colors: ListItemColors::default(),
1482            tonal_elevation: 0.0,
1483            shadow_elevation: 0.0,
1484            shape_radius: 0.0,
1485            horizontal_padding: ListItemDefaults::HORIZONTAL_PADDING,
1486            trailing_padding: ListItemDefaults::TRAILING_PADDING,
1487            one_line_height: ListItemDefaults::ONE_LINE_HEIGHT,
1488            two_line_height: ListItemDefaults::TWO_LINE_HEIGHT,
1489            three_line_height: ListItemDefaults::THREE_LINE_HEIGHT,
1490        }
1491    }
1492}
1493
1494/// M3 List Item - a single row in a list with optional leading/trailing content.
1495pub fn ListItem(
1496    headline: impl Into<String>,
1497    supporting_text: Option<String>,
1498    leading: Option<View>,
1499    trailing: Option<View>,
1500    on_click: Option<Rc<dyn Fn()>>,
1501    config: ListItemConfig,
1502) -> View {
1503    let th = theme();
1504    let is_enabled = config.enabled;
1505    let c = &config.colors;
1506
1507    let hd_col = c.headline(is_enabled, false, false);
1508    let sp_col = c.supporting(is_enabled, false, false);
1509    let ld_col = c.leading_icon(is_enabled, false, false);
1510    let tr_col = c.trailing_icon(is_enabled, false, false);
1511    let bg = c.container(is_enabled, false, false);
1512
1513    let line_count = if supporting_text.is_some() { 2 } else { 1 };
1514    let min_h = match line_count {
1515        2 => config.two_line_height,
1516        3 => config.three_line_height,
1517        _ => config.one_line_height,
1518    };
1519
1520    let mut modifier = Modifier::new()
1521        .min_width(200.0)
1522        .min_height(min_h)
1523        .background(bg)
1524        .clip_rounded(config.shape_radius)
1525        .state_colors(StateColors {
1526            default: Color::TRANSPARENT,
1527            hovered: th.on_surface.with_alpha_f32(0.08),
1528            pressed: th.on_surface.with_alpha_f32(0.12),
1529            disabled: Color::TRANSPARENT,
1530        })
1531        .padding_values(PaddingValues {
1532            left: config.horizontal_padding,
1533            right: config.trailing_padding,
1534            top: 8.0,
1535            bottom: 8.0,
1536        })
1537        .align_items(AlignItems::Center)
1538        .then(config.modifier);
1539
1540    if config.tonal_elevation > 0.0 {
1541        modifier = modifier.state_elevation(StateElevation {
1542            default: config.tonal_elevation,
1543            hovered: config.tonal_elevation,
1544            pressed: config.tonal_elevation,
1545            disabled: 0.0,
1546        });
1547    }
1548    if config.shadow_elevation > 0.0 {
1549        modifier = modifier.shadow(config.shadow_elevation, 0.0);
1550    }
1551
1552    if let Some(cb) = on_click {
1553        let cb = cb.clone();
1554        modifier = modifier.clickable().on_pointer_down(move |_| {
1555            if is_enabled {
1556                cb();
1557            }
1558        });
1559    }
1560
1561    let wrap_icon = |color: Color, v: View| -> View { with_content_color(color, move || v) };
1562
1563    Row(modifier).child((
1564        leading
1565            .map(|v| {
1566                Box(Modifier::new().padding_values(PaddingValues {
1567                    left: 0.0,
1568                    right: 16.0,
1569                    top: 0.0,
1570                    bottom: 0.0,
1571                }))
1572                .child(wrap_icon(ld_col, v))
1573            })
1574            .unwrap_or(Box(Modifier::new())),
1575        Column(
1576            Modifier::new()
1577                .flex_grow(1.0)
1578                .justify_content(JustifyContent::Center),
1579        )
1580        .child((
1581            Text(headline)
1582                .color(hd_col)
1583                .size(th.typography.body_large)
1584                .single_line(),
1585            supporting_text
1586                .map(|st| {
1587                    Text(st)
1588                        .color(sp_col)
1589                        .size(th.typography.body_medium)
1590                        .max_lines(2)
1591                        .overflow_ellipsize()
1592                })
1593                .unwrap_or(Box(Modifier::new())),
1594        )),
1595        trailing
1596            .map(|v| {
1597                Box(Modifier::new().padding_values(PaddingValues {
1598                    left: 16.0,
1599                    right: 0.0,
1600                    top: 0.0,
1601                    bottom: 0.0,
1602                }))
1603                .child(wrap_icon(tr_col, v))
1604            })
1605            .unwrap_or(Box(Modifier::new())),
1606    ))
1607}
1608
1609/// A single tab definition for use with `TabRow`.
1610pub struct Tab {
1611    pub label: String,
1612    pub icon: Option<View>,
1613    pub on_click: Rc<dyn Fn()>,
1614    pub enabled: bool,
1615}
1616
1617/// Configuration for [`TabRow`].
1618#[derive(Clone, Debug)]
1619pub struct TabRowConfig {
1620    pub modifier: Modifier,
1621    pub container_color: Color,
1622    pub selected_content_color: Color,
1623    pub unselected_content_color: Color,
1624    pub indicator_color: Color,
1625    pub height: f32,
1626    pub indicator_height: f32,
1627}
1628
1629impl Default for TabRowConfig {
1630    fn default() -> Self {
1631        Self {
1632            modifier: Modifier::new(),
1633            container_color: TabDefaults::container_color(),
1634            selected_content_color: TabDefaults::selected_content_color(),
1635            unselected_content_color: TabDefaults::unselected_content_color(),
1636            indicator_color: TabDefaults::indicator_color(),
1637            height: TabDefaults::HEIGHT,
1638            indicator_height: TabDefaults::INDICATOR_HEIGHT,
1639        }
1640    }
1641}
1642
1643static TABROW_COUNTER: AtomicU64 = AtomicU64::new(0);
1644
1645/// M3 Tab Row — a horizontal row of tabs with per-tab animated-height indicators.
1646/// Text colors animate with DefaultEffects (spring_crit 40.0).
1647/// Indicator height animates with DefaultEffects (spring_crit 40.0).
1648pub fn TabRow(selected_index: usize, tabs: Vec<Tab>, config: TabRowConfig) -> View {
1649    let th = theme();
1650    let id = remember(|| TABROW_COUNTER.fetch_add(1, Ordering::Relaxed));
1651    let default_effects = AnimationSpec::spring_crit(40.0);
1652    Column(Modifier::new().fill_max_width().then(config.modifier)).child((
1653        Row(Modifier::new()
1654            .fill_max_width()
1655            .height(config.height)
1656            .background(config.container_color)
1657            .semantics(Semantics::new(Role::Container).with_selectable_group()))
1658        .child(
1659            tabs.into_iter()
1660                .enumerate()
1661                .map(|(i, tab)| {
1662                    let selected = i == selected_index;
1663                    let is_enabled = tab.enabled;
1664                    let color = animate_color(
1665                        format!("tab_clr_{}_{}", id, i),
1666                        if selected {
1667                            config.selected_content_color
1668                        } else {
1669                            config.unselected_content_color
1670                        },
1671                        default_effects,
1672                    );
1673                    let indicator_h = animate_f32(
1674                        format!("tab_ind_h_{}_{}", id, i),
1675                        if selected {
1676                            config.indicator_height
1677                        } else {
1678                            0.0
1679                        },
1680                        default_effects,
1681                    );
1682                    let cb = tab.on_click.clone();
1683
1684                    let mut tab_m = Modifier::new()
1685                        .flex_grow(1.0)
1686                        .fill_max_height()
1687                        .align_items(AlignItems::Center)
1688                        .justify_content(JustifyContent::Center)
1689                        .state_colors(StateColors {
1690                            default: Color::TRANSPARENT,
1691                            hovered: th.on_surface.with_alpha_f32(0.08),
1692                            pressed: th.on_surface.with_alpha_f32(0.12),
1693                            disabled: Color::TRANSPARENT,
1694                        })
1695                        .semantics(Semantics::new(Role::Tab).with_label(&tab.label));
1696
1697                    if is_enabled {
1698                        tab_m = tab_m.clickable().on_pointer_down(move |_| cb());
1699                    }
1700
1701                    Column(tab_m).child((
1702                        tab.icon.unwrap_or(Box(Modifier::new())),
1703                        Text(tab.label)
1704                            .color(color)
1705                            .size(th.typography.title_small)
1706                            .single_line(),
1707                        Box(Modifier::new()
1708                            .fill_max_width()
1709                            .height(indicator_h)
1710                            .background(config.indicator_color)
1711                            .clip_rounded(TabDefaults::INDICATOR_CORNER)),
1712                    ))
1713                })
1714                .collect::<Vec<_>>(),
1715        ),
1716        // Divider
1717        Box(Modifier::new()
1718            .fill_max_width()
1719            .height(1.0)
1720            .background(th.outline_variant)),
1721    ))
1722}
1723
1724/// A single segment definition for `SegmentedButton`.
1725pub struct Segment {
1726    pub label: String,
1727    pub icon: Option<View>,
1728    pub on_click: Rc<dyn Fn()>,
1729    pub enabled: bool,
1730}
1731
1732/// Configuration for [`SegmentedButton`].
1733#[derive(Clone, Debug)]
1734pub struct SegmentedButtonConfig {
1735    pub modifier: Modifier,
1736    pub border_color: Color,
1737    pub selected_container_color: Color,
1738    pub selected_content_color: Color,
1739    pub unselected_content_color: Color,
1740    pub state_colors: StateColors,
1741    pub height: f32,
1742    pub shape_radius: f32,
1743}
1744
1745impl Default for SegmentedButtonConfig {
1746    fn default() -> Self {
1747        Self {
1748            modifier: Modifier::new(),
1749            border_color: SegmentedButtonDefaults::border_color(),
1750            selected_container_color: SegmentedButtonDefaults::selected_container_color(),
1751            selected_content_color: SegmentedButtonDefaults::selected_content_color(),
1752            unselected_content_color: SegmentedButtonDefaults::unselected_content_color(),
1753            state_colors: SegmentedButtonDefaults::state_colors_default(),
1754            height: SegmentedButtonDefaults::HEIGHT,
1755            shape_radius: SegmentedButtonDefaults::SHAPE_RADIUS,
1756        }
1757    }
1758}
1759
1760static SEGBUTTON_COUNTER: AtomicU64 = AtomicU64::new(0);
1761
1762/// M3 Segmented Button - a row of toggle segments. `selected` contains the
1763/// indices of selected segments (single-select: pass a single-element set).
1764/// Each segment is shaped independently: first has rounded left corners,
1765/// last has rounded right corners, middle segments are rectangular.
1766pub fn SegmentedButton(
1767    selected: &[usize],
1768    segments: Vec<Segment>,
1769    config: SegmentedButtonConfig,
1770) -> View {
1771    let th = theme();
1772    let count = segments.len();
1773    let id = remember(|| SEGBUTTON_COUNTER.fetch_add(1, Ordering::Relaxed));
1774    let spec = th.motion.color;
1775    let shape_r = config.shape_radius;
1776
1777    // corner order: [BL, BR, TR, TL]
1778    let segment_radii = |i: usize| -> [f32; 4] {
1779        if count == 1 {
1780            [shape_r, shape_r, shape_r, shape_r]
1781        } else if i == 0 {
1782            [shape_r, 0.0, 0.0, shape_r]
1783        } else if i == count - 1 {
1784            [0.0, shape_r, shape_r, 0.0]
1785        } else {
1786            [0.0, 0.0, 0.0, 0.0]
1787        }
1788    };
1789
1790    // Outer border wraps the entire group. Internal dividers are inside each segment Row.
1791    Row(Modifier::new()
1792        .height(config.height)
1793        .border(1.0, config.border_color, shape_r)
1794        .then(config.modifier))
1795    .child(
1796        segments
1797            .into_iter()
1798            .enumerate()
1799            .map(|(i, seg)| {
1800                let is_selected = selected.contains(&i);
1801
1802                let bg = animate_color(
1803                    format!("sb_bg_{}_{}", id, i),
1804                    if is_selected {
1805                        config.selected_container_color
1806                    } else {
1807                        Color::TRANSPARENT
1808                    },
1809                    spec,
1810                );
1811                let fg = animate_color(
1812                    format!("sb_fg_{}_{}", id, i),
1813                    if is_selected {
1814                        config.selected_content_color
1815                    } else {
1816                        config.unselected_content_color
1817                    },
1818                    spec,
1819                );
1820
1821                let cb = seg.on_click.clone();
1822                let radii = segment_radii(i);
1823                let is_enabled = seg.enabled;
1824
1825                let state_colors = config.state_colors;
1826                let content_modifier = Modifier::new()
1827                    .flex_grow(1.0)
1828                    .fill_max_height()
1829                    .clip_rounded_radii(radii)
1830                    .background(bg)
1831                    .state_colors(state_colors)
1832                    .align_items(AlignItems::Center)
1833                    .justify_content(JustifyContent::Center)
1834                    .padding_values(PaddingValues {
1835                        left: 12.0,
1836                        right: 12.0,
1837                        top: 0.0,
1838                        bottom: 0.0,
1839                    });
1840
1841                let content_modifier = if is_enabled {
1842                    content_modifier.clickable().on_pointer_down(move |_| cb())
1843                } else {
1844                    content_modifier
1845                };
1846
1847                Row(Modifier::new().flex_grow(1.0).fill_max_height()).child((
1848                    Row(content_modifier).child((
1849                        seg.icon.unwrap_or(Box(Modifier::new())),
1850                        Text(seg.label)
1851                            .color(fg)
1852                            .size(th.typography.label_large)
1853                            .single_line(),
1854                    )),
1855                    if i < count - 1 {
1856                        Box(Modifier::new()
1857                            .width(1.0)
1858                            .fill_max_height()
1859                            .background(th.outline))
1860                    } else {
1861                        Box(Modifier::new())
1862                    },
1863                ))
1864            })
1865            .collect::<Vec<_>>(),
1866    )
1867}
1868
1869/// Configuration for [`CircularProgressIndicator`].
1870#[derive(Clone, Debug)]
1871pub struct CircularProgressIndicatorConfig {
1872    pub color: Color,
1873    pub track_color: Color,
1874    pub stroke_width: f32,
1875    pub stroke_cap: StrokeCap,
1876    pub gap_size: f32,
1877}
1878
1879impl Default for CircularProgressIndicatorConfig {
1880    fn default() -> Self {
1881        Self {
1882            color: ProgressIndicatorDefaults::circular_color(),
1883            track_color: ProgressIndicatorDefaults::circular_track_color(),
1884            stroke_width: ProgressIndicatorDefaults::CIRCULAR_STROKE_WIDTH,
1885            stroke_cap: StrokeCap::Round,
1886            gap_size: 0.0,
1887        }
1888    }
1889}
1890
1891/// M3 Circular Progress Indicator.
1892///
1893/// Determinate (`Some(0..1)`): draws arc from 12 o'clock clockwise.
1894/// Indeterminate (`None`): animates a spinning 270° arc.
1895pub fn CircularProgressIndicator(
1896    value: Option<f32>,
1897    config: CircularProgressIndicatorConfig,
1898) -> View {
1899    let sz = dp_to_px(ProgressIndicatorDefaults::CIRCULAR_INDICATOR_SIZE);
1900    let stroke_px = dp_to_px(config.stroke_width);
1901    let val = value.map(|v| v.clamp(0.0, 1.0));
1902
1903    // Three concurrent animations matching Compose Material3 indeterminate spec:
1904    //   1. Global rotation — 1080° linear over 6000ms
1905    //   2. Additional rotation — 90° stepped jumps with EmphasizedDecelerate
1906    //   3. Sweep — oscillates 0.1 → 0.87 → 0.1 over 6000ms
1907    let (global_rotation, additional_rotation, sweep_val) = if value.is_none() {
1908        let shared = remember_state_with_key("circ_ind_shared", || {
1909            let mut a = AnimatedValue::new(
1910                0.0f32,
1911                AnimationSpec::tween(Duration::from_millis(6000), Easing::Linear)
1912                    .repeated(RepeatableSpec::infinite()),
1913            );
1914            a.set_target(1.0);
1915            a
1916        });
1917        let mut s = shared.borrow_mut();
1918        s.update();
1919        let t = *s.get();
1920        drop(s);
1921
1922        let gv = t * 1080.0;
1923
1924        let emph = Easing::Custom(CubicBezier::new(0.05, 0.7, 0.1, 1.0));
1925        let add_kf = remember_state_with_key("circ_ind_add_kf", || KeyframesSpec {
1926            keyframes: vec![
1927                (0.0, 0.0, None),
1928                (0.05, 90.0, Some(emph)),
1929                (0.25, 90.0, None),
1930                (0.30, 180.0, None),
1931                (0.50, 180.0, None),
1932                (0.55, 270.0, None),
1933                (0.75, 270.0, None),
1934                (0.80, 360.0, None),
1935                (1.0, 360.0, None),
1936            ],
1937        });
1938        let av = add_kf.borrow().evaluate(t);
1939
1940        let std_dec = Easing::Custom(CubicBezier::new(0.2, 0.0, 0.0, 1.0));
1941        let sweep_kf = remember_state_with_key("circ_ind_sweep_kf", || KeyframesSpec {
1942            keyframes: vec![
1943                (0.0, 0.1, None),
1944                (0.5, 0.87, Some(std_dec)),
1945                (1.0, 0.1, None),
1946            ],
1947        });
1948        let sv = sweep_kf.borrow().evaluate(t);
1949
1950        (gv, av, sv)
1951    } else {
1952        (0.0, 0.0, 0.0)
1953    };
1954
1955    // Pre-compute gap angular size in radians
1956    let indicator_size_dp = ProgressIndicatorDefaults::CIRCULAR_INDICATOR_SIZE;
1957    let adjusted_gap_dp = if config.stroke_cap == StrokeCap::Butt {
1958        config.gap_size
1959    } else {
1960        config.gap_size + config.stroke_width
1961    };
1962    let circle_dia_dp = indicator_size_dp - config.stroke_width;
1963    let gap_sweep_rad = 2.0 * adjusted_gap_dp / circle_dia_dp;
1964
1965    Box(Modifier::new()
1966        .size(sz, sz)
1967        .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
1968            let mul_c = |c: Color| {
1969                Color(
1970                    c.0,
1971                    c.1,
1972                    c.2,
1973                    ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
1974                )
1975            };
1976            let cx = rect.x + rect.w * 0.5;
1977            let cy = rect.y + rect.h * 0.5;
1978            let r = (rect.w.min(rect.h)) * 0.5 - stroke_px * 0.5;
1979            let circle = Rect {
1980                x: cx - r,
1981                y: cy - r,
1982                w: r * 2.0,
1983                h: r * 2.0,
1984            };
1985
1986            match val {
1987                Some(p) => {
1988                    let sweep_rad = p * std::f32::consts::TAU;
1989                    let start_angle = -std::f32::consts::FRAC_PI_2;
1990                    let effective_gap = gap_sweep_rad.min(sweep_rad);
1991
1992                    // Indicator arc
1993                    if p > 0.0 {
1994                        scene.nodes.push(SceneNode::Arc {
1995                            rect: circle,
1996                            start_angle,
1997                            sweep_angle: sweep_rad,
1998                            stroke_width: stroke_px,
1999                            color: mul_c(config.color),
2000                            cap: config.stroke_cap,
2001                        });
2002                    }
2003
2004                    // Track arc (with gap from indicator)
2005                    let track_start = start_angle + sweep_rad + effective_gap;
2006                    let track_sweep = std::f32::consts::TAU - sweep_rad - 2.0 * effective_gap;
2007                    if track_sweep > 0.0 {
2008                        scene.nodes.push(SceneNode::Arc {
2009                            rect: circle,
2010                            start_angle: track_start,
2011                            sweep_angle: track_sweep,
2012                            stroke_width: stroke_px,
2013                            color: mul_c(config.track_color),
2014                            cap: config.stroke_cap,
2015                        });
2016                    }
2017                }
2018                None => {
2019                    let radians =
2020                        (global_rotation + additional_rotation) * std::f32::consts::PI / 180.0;
2021                    let start_angle = -std::f32::consts::FRAC_PI_2 + radians;
2022                    let sweep_rad = sweep_val * std::f32::consts::TAU;
2023                    let effective_gap = gap_sweep_rad.min(sweep_rad);
2024
2025                    // Indicator arc
2026                    scene.nodes.push(SceneNode::Arc {
2027                        rect: circle,
2028                        start_angle,
2029                        sweep_angle: sweep_rad,
2030                        stroke_width: stroke_px,
2031                        color: mul_c(config.color),
2032                        cap: config.stroke_cap,
2033                    });
2034
2035                    // Track arc (with gap from indicator)
2036                    let track_start = start_angle + sweep_rad + effective_gap;
2037                    let track_sweep = std::f32::consts::TAU - sweep_rad - 2.0 * effective_gap;
2038                    if track_sweep > 0.0 {
2039                        scene.nodes.push(SceneNode::Arc {
2040                            rect: circle,
2041                            start_angle: track_start,
2042                            sweep_angle: track_sweep,
2043                            stroke_width: stroke_px,
2044                            color: mul_c(config.track_color),
2045                            cap: config.stroke_cap,
2046                        });
2047                    }
2048                }
2049            }
2050        }))
2051    .semantics(Semantics {
2052        role: Role::ProgressBar,
2053        label: None,
2054        focused: false,
2055        enabled: true,
2056        selectable_group: false,
2057    })
2058}
2059
2060/// Configuration for [`LinearProgressIndicator`].
2061#[derive(Clone, Debug)]
2062pub struct LinearProgressIndicatorConfig {
2063    pub color: Color,
2064    pub track_color: Color,
2065    /// Stroke cap style for the indicator ends. Default: `StrokeCap::Round`
2066    pub stroke_cap: StrokeCap,
2067    /// Gap between indicator and track, in dp.
2068    pub gap_size: f32,
2069    /// Diameter of the stop indicator dot, in dp.
2070    pub stop_size: f32,
2071}
2072
2073impl Default for LinearProgressIndicatorConfig {
2074    fn default() -> Self {
2075        Self {
2076            color: ProgressIndicatorDefaults::linear_color(),
2077            track_color: ProgressIndicatorDefaults::linear_track_color(),
2078            stroke_cap: StrokeCap::Round,
2079            gap_size: ProgressIndicatorDefaults::LINEAR_INDICATOR_GAP_SIZE,
2080            stop_size: ProgressIndicatorDefaults::LINEAR_TRACK_STOP_SIZE,
2081        }
2082    }
2083}
2084
2085/// M3 Linear Progress Indicator.
2086///
2087/// Pass `LinearProgressIndicatorConfig::default()` for standard M3 appearance,
2088/// or override individual fields via struct-update syntax.
2089pub fn LinearProgressIndicator(value: Option<f32>, config: LinearProgressIndicatorConfig) -> View {
2090    Box(Modifier::new()
2091        .fill_max_width()
2092        .height(ProgressIndicatorDefaults::LINEAR_INDICATOR_HEIGHT)
2093        .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
2094            let mul_c = |c: Color| {
2095                Color(
2096                    c.0,
2097                    c.1,
2098                    c.2,
2099                    ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
2100                )
2101            };
2102            let track_h = rect.h;
2103            let corner = track_h * 0.5;
2104            let dot_r = dp_to_px(config.stop_size) * 0.5;
2105            let cy = rect.y + rect.h * 0.5;
2106            let t = value.unwrap_or(0.0).clamp(0.0, 1.0);
2107
2108            let cap_radius = if config.stroke_cap == StrokeCap::Butt {
2109                0.0
2110            } else {
2111                corner
2112            };
2113
2114            let gap = dp_to_px(config.gap_size)
2115                - if config.stroke_cap == StrokeCap::Butt {
2116                    0.0
2117                } else {
2118                    cap_radius
2119                };
2120
2121            let cap_ofs = cap_radius;
2122            let ind_end = (t * rect.w).clamp(cap_ofs, rect.w - cap_ofs);
2123            let ind_w = (ind_end - cap_ofs).max(0.0);
2124
2125            // Indicator (active portion from left)
2126            if t > 0.0 && ind_w > 0.0 {
2127                scene.nodes.push(SceneNode::Rect {
2128                    rect: Rect {
2129                        x: rect.x + cap_ofs,
2130                        y: cy - corner,
2131                        w: ind_w,
2132                        h: track_h,
2133                    },
2134                    brush: Brush::Solid(mul_c(config.color)),
2135                    radius: [cap_radius; 4],
2136                });
2137            }
2138
2139            // Track (inactive portion after gap)
2140            let track_start = (rect.x + ind_end + gap).min(rect.x + rect.w);
2141            let track_w = (rect.x + rect.w - track_start).max(0.0);
2142            if t < 1.0 && track_w > 0.0 {
2143                let track_left = track_start + cap_ofs;
2144                let track_right = rect.x + rect.w;
2145                if track_right > track_left {
2146                    scene.nodes.push(SceneNode::Rect {
2147                        rect: Rect {
2148                            x: track_left,
2149                            y: cy - corner,
2150                            w: track_right - track_left,
2151                            h: track_h,
2152                        },
2153                        brush: Brush::Solid(mul_c(config.track_color)),
2154                        radius: [cap_radius; 4],
2155                    });
2156                }
2157            }
2158
2159            // Stop indicator at right end circle
2160            {
2161                let sx = rect.x + rect.w - dot_r;
2162                scene.nodes.push(SceneNode::Ellipse {
2163                    rect: Rect {
2164                        x: sx - dot_r,
2165                        y: cy - dot_r,
2166                        w: dot_r * 2.0,
2167                        h: dot_r * 2.0,
2168                    },
2169                    brush: Brush::Solid(mul_c(config.color)),
2170                });
2171            }
2172        }))
2173    .semantics(Semantics {
2174        role: Role::ProgressBar,
2175        label: None,
2176        focused: false,
2177        enabled: true,
2178        selectable_group: false,
2179    })
2180}
2181
2182/// Color slots for text fields — matches Compose Material3 `TextFieldColors`.
2183/// All 42 color fields (focused/unfocused/disabled/error variants of each slot).
2184#[allow(dead_code)]
2185#[derive(Clone, Debug)]
2186pub struct TextFieldColors {
2187    pub focused_text_color: Color,
2188    pub unfocused_text_color: Color,
2189    pub disabled_text_color: Color,
2190    pub error_text_color: Color,
2191    pub focused_container_color: Color,
2192    pub unfocused_container_color: Color,
2193    pub disabled_container_color: Color,
2194    pub error_container_color: Color,
2195    pub cursor_color: Color,
2196    pub error_cursor_color: Color,
2197    pub focused_indicator_color: Color,
2198    pub unfocused_indicator_color: Color,
2199    pub disabled_indicator_color: Color,
2200    pub error_indicator_color: Color,
2201    pub focused_leading_icon_color: Color,
2202    pub unfocused_leading_icon_color: Color,
2203    pub disabled_leading_icon_color: Color,
2204    pub error_leading_icon_color: Color,
2205    pub focused_trailing_icon_color: Color,
2206    pub unfocused_trailing_icon_color: Color,
2207    pub disabled_trailing_icon_color: Color,
2208    pub error_trailing_icon_color: Color,
2209    pub focused_label_color: Color,
2210    pub unfocused_label_color: Color,
2211    pub disabled_label_color: Color,
2212    pub error_label_color: Color,
2213    pub focused_placeholder_color: Color,
2214    pub unfocused_placeholder_color: Color,
2215    pub disabled_placeholder_color: Color,
2216    pub error_placeholder_color: Color,
2217    pub focused_supporting_text_color: Color,
2218    pub unfocused_supporting_text_color: Color,
2219    pub disabled_supporting_text_color: Color,
2220    pub error_supporting_text_color: Color,
2221    pub focused_prefix_color: Color,
2222    pub unfocused_prefix_color: Color,
2223    pub disabled_prefix_color: Color,
2224    pub error_prefix_color: Color,
2225    pub focused_suffix_color: Color,
2226    pub unfocused_suffix_color: Color,
2227    pub disabled_suffix_color: Color,
2228    pub error_suffix_color: Color,
2229}
2230
2231#[allow(dead_code)]
2232impl TextFieldColors {
2233    pub fn text_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2234        if !enabled {
2235            self.disabled_text_color
2236        } else if is_error {
2237            self.error_text_color
2238        } else if focused {
2239            self.focused_text_color
2240        } else {
2241            self.unfocused_text_color
2242        }
2243    }
2244    pub fn container_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2245        if !enabled {
2246            self.disabled_container_color
2247        } else if is_error {
2248            self.error_container_color
2249        } else if focused {
2250            self.focused_container_color
2251        } else {
2252            self.unfocused_container_color
2253        }
2254    }
2255    pub fn cursor_color(&self, is_error: bool) -> Color {
2256        if is_error {
2257            self.error_cursor_color
2258        } else {
2259            self.cursor_color
2260        }
2261    }
2262    pub fn indicator_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2263        if !enabled {
2264            self.disabled_indicator_color
2265        } else if is_error {
2266            self.error_indicator_color
2267        } else if focused {
2268            self.focused_indicator_color
2269        } else {
2270            self.unfocused_indicator_color
2271        }
2272    }
2273    pub fn leading_icon_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2274        if !enabled {
2275            self.disabled_leading_icon_color
2276        } else if is_error {
2277            self.error_leading_icon_color
2278        } else if focused {
2279            self.focused_leading_icon_color
2280        } else {
2281            self.unfocused_leading_icon_color
2282        }
2283    }
2284    pub fn trailing_icon_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2285        if !enabled {
2286            self.disabled_trailing_icon_color
2287        } else if is_error {
2288            self.error_trailing_icon_color
2289        } else if focused {
2290            self.focused_trailing_icon_color
2291        } else {
2292            self.unfocused_trailing_icon_color
2293        }
2294    }
2295    pub fn label_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2296        if !enabled {
2297            self.disabled_label_color
2298        } else if is_error {
2299            self.error_label_color
2300        } else if focused {
2301            self.focused_label_color
2302        } else {
2303            self.unfocused_label_color
2304        }
2305    }
2306    pub fn placeholder_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2307        if !enabled {
2308            self.disabled_placeholder_color
2309        } else if is_error {
2310            self.error_placeholder_color
2311        } else if focused {
2312            self.focused_placeholder_color
2313        } else {
2314            self.unfocused_placeholder_color
2315        }
2316    }
2317    pub fn supporting_text_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2318        if !enabled {
2319            self.disabled_supporting_text_color
2320        } else if is_error {
2321            self.error_supporting_text_color
2322        } else if focused {
2323            self.focused_supporting_text_color
2324        } else {
2325            self.unfocused_supporting_text_color
2326        }
2327    }
2328}
2329
2330/// Default values for text field colors.
2331pub struct TextFieldDefaults;
2332
2333impl TextFieldDefaults {
2334    pub fn colors() -> TextFieldColors {
2335        let th = theme();
2336        TextFieldColors {
2337            focused_text_color: th.on_surface,
2338            unfocused_text_color: th.on_surface,
2339            disabled_text_color: th.on_surface.with_alpha_f32(0.38),
2340            error_text_color: th.on_surface,
2341            focused_container_color: th.surface_container_highest,
2342            unfocused_container_color: th.surface_container_highest,
2343            disabled_container_color: th.on_surface.with_alpha_f32(0.04),
2344            error_container_color: th.surface_container_highest,
2345            cursor_color: th.primary,
2346            error_cursor_color: th.error,
2347            focused_indicator_color: th.primary,
2348            unfocused_indicator_color: th.on_surface_variant,
2349            disabled_indicator_color: th.on_surface.with_alpha_f32(0.12),
2350            error_indicator_color: th.error,
2351            focused_leading_icon_color: th.on_surface_variant,
2352            unfocused_leading_icon_color: th.on_surface_variant,
2353            disabled_leading_icon_color: th.on_surface.with_alpha_f32(0.38),
2354            error_leading_icon_color: th.error,
2355            focused_trailing_icon_color: th.on_surface_variant,
2356            unfocused_trailing_icon_color: th.on_surface_variant,
2357            disabled_trailing_icon_color: th.on_surface.with_alpha_f32(0.38),
2358            error_trailing_icon_color: th.error,
2359            focused_label_color: th.primary,
2360            unfocused_label_color: th.on_surface_variant,
2361            disabled_label_color: th.on_surface.with_alpha_f32(0.38),
2362            error_label_color: th.error,
2363            focused_placeholder_color: th.on_surface_variant,
2364            unfocused_placeholder_color: th.on_surface_variant,
2365            disabled_placeholder_color: th.on_surface.with_alpha_f32(0.38),
2366            error_placeholder_color: th.error,
2367            focused_supporting_text_color: th.on_surface_variant,
2368            unfocused_supporting_text_color: th.on_surface_variant,
2369            disabled_supporting_text_color: th.on_surface.with_alpha_f32(0.38),
2370            error_supporting_text_color: th.error,
2371            focused_prefix_color: th.on_surface,
2372            unfocused_prefix_color: th.on_surface,
2373            disabled_prefix_color: th.on_surface.with_alpha_f32(0.38),
2374            error_prefix_color: th.on_surface,
2375            focused_suffix_color: th.on_surface,
2376            unfocused_suffix_color: th.on_surface,
2377            disabled_suffix_color: th.on_surface.with_alpha_f32(0.38),
2378            error_suffix_color: th.on_surface,
2379        }
2380    }
2381}
2382
2383/// Configuration for an `OutlinedTextField`.
2384#[derive(Clone)]
2385pub struct OutlinedTextFieldConfig {
2386    /// Floating label shown above the input when the field has text or is focused.
2387    /// When set, this acts as the visual placeholder (the TextField's own placeholder
2388    /// is suppressed). When the label floats, it animates to the top border.
2389    pub label: Option<String>,
2390    /// Placeholder text shown inside the TextField when empty and unfocused.
2391    /// Only shown when `label` is `None`; when a label is present the label
2392    /// itself serves as the visual placeholder.
2393    pub placeholder: Option<String>,
2394    /// Icon displayed at the start of the input.
2395    pub leading_icon: Option<View>,
2396    /// Icon displayed at the end of the input.
2397    pub trailing_icon: Option<View>,
2398    /// If true, Enter submits; if false, Enter inserts a newline.
2399    pub single_line: bool,
2400    /// If true, border and label color switch to error color.
2401    pub is_error: bool,
2402    /// If false, input is visually disabled and `on_value_change` won't fire.
2403    pub enabled: bool,
2404    /// Called when the user presses Enter on a single-line field.
2405    pub on_submit: Option<Rc<dyn Fn(String)>>,
2406    /// Colors for all text field UI elements.
2407    pub colors: Option<TextFieldColors>,
2408}
2409
2410impl Default for OutlinedTextFieldConfig {
2411    fn default() -> Self {
2412        Self {
2413            label: None,
2414            placeholder: None,
2415            leading_icon: None,
2416            trailing_icon: None,
2417            single_line: true,
2418            is_error: false,
2419            enabled: true,
2420            on_submit: None,
2421            colors: None,
2422        }
2423    }
2424}
2425
2426/// M3 Outlined Text Field with floating label, leading/trailing icons, and error state.
2427///
2428/// The label floats up when `value` is non-empty or when the field is focused.
2429/// Note: focus-based floating is approximated via animated `float_t` - the label
2430/// begins floating once `on_value_change` fires (i.e. when the user types).
2431/// For strict focus-on-tap floating, pair with an external focus signal.
2432///
2433/// # Example
2434/// ```ignore
2435/// let text = remember(|| signal(String::new()));
2436/// OutlinedTextField(
2437///     Modifier::new().fill_max_width().padding(16.0),
2438///     text.get(),
2439///     { let t = text.clone(); move |v| t.set(v) },
2440///     OutlinedTextFieldConfig {
2441///         label: Some("Email".into()),
2442///         placeholder: Some("user@example.com".into()),
2443///         ..Default::default()
2444///     },
2445/// );
2446/// ```
2447pub fn OutlinedTextField(
2448    modifier: Modifier,
2449    value: String,
2450    on_value_change: impl Fn(String) + 'static,
2451    config: OutlinedTextFieldConfig,
2452) -> View {
2453    let th = theme();
2454    let label_str: Option<Rc<str>> = config.label.map(Rc::from);
2455    let has_label = label_str.is_some();
2456
2457    // Unique animation key per label to avoid conflicts when multiple fields exist
2458    let anim_key = match &label_str {
2459        Some(l) => format!("otf_{}", &l[..l.len().min(32)]),
2460        None => "otf_nolabel".into(),
2461    };
2462
2463    // Persistent focus tracker - set by layout/paint when this field is focused,
2464    // read here on the next frame. This gives a one-frame delay on tap-to-float,
2465    // which is negligible at 60fps.
2466    let focus_tracker: Rc<Cell<bool>> =
2467        remember_with_key(format!("otf_focus_{}", anim_key), || Cell::new(false));
2468    let is_focused = focus_tracker.get();
2469    let should_float = !value.is_empty() || is_focused;
2470
2471    let float_t = animate_f32(
2472        anim_key.clone(),
2473        if should_float { 1.0 } else { 0.0 },
2474        th.motion.color,
2475    );
2476
2477    // Resolve colors from config.colors if set, otherwise use legacy theme-based approach
2478    let (border_color, label_color) = if let Some(ref tc) = config.colors {
2479        let enf = config.enabled && is_focused;
2480        (
2481            tc.indicator_color(config.enabled, config.is_error, enf),
2482            tc.label_color(config.enabled, config.is_error, enf),
2483        )
2484    } else {
2485        (
2486            if config.is_error {
2487                th.error
2488            } else if float_t > 0.5 {
2489                th.primary
2490            } else {
2491                th.outline
2492            },
2493            if config.is_error {
2494                th.error
2495            } else if float_t > 0.5 {
2496                th.primary
2497            } else {
2498                th.on_surface_variant
2499            },
2500        )
2501    };
2502
2503    // Label font size: 16dp at rest (placeholder position) → 12dp when floating
2504    let label_size = 16.0 - 4.0 * float_t;
2505
2506    // Label Y offset: 16dp (same line as text) → -4dp (overlapping top border)
2507    let label_y = 16.0 - 20.0 * float_t;
2508
2509    // The TextField inside uses no placeholder when a label is present -
2510    // the label itself serves as the visual placeholder.
2511    let tf_placeholder = if has_label {
2512        String::new()
2513    } else {
2514        config.placeholder.unwrap_or_default()
2515    };
2516
2517    Box(modifier
2518        .clip_rounded(th.shapes.small)
2519        .border(1.0, border_color, th.shapes.small)
2520        .background(th.surface))
2521    .child(
2522        Stack(Modifier::new().fill_max_size()).child((
2523            // Input row - always at the same position, with room at the top
2524            // for the floating label to overlap.
2525            Row(Modifier::new()
2526                .fill_max_size()
2527                .padding_values(PaddingValues {
2528                    left: 16.0,
2529                    right: 16.0,
2530                    top: 16.0,
2531                    bottom: 8.0,
2532                })
2533                .align_items(AlignItems::Center))
2534            .child((
2535                config.leading_icon.unwrap_or(Box(Modifier::new())),
2536                View::new(0, ViewKind::Box)
2537                    .modifier(
2538                        Modifier::new()
2539                            .flex_grow(1.0)
2540                            .padding_values(PaddingValues {
2541                                left: 8.0,
2542                                right: 8.0,
2543                                top: 0.0,
2544                                bottom: 0.0,
2545                            })
2546                            .text_input(TextInputConfig {
2547                                hint: tf_placeholder,
2548                                multiline: false,
2549                                on_change: Some(Rc::new(on_value_change) as _),
2550                                on_submit: config.on_submit.clone().map(|f| {
2551                                    let f = f.clone();
2552                                    Rc::new(move |s| f(s)) as Rc<dyn Fn(String)>
2553                                }),
2554                                focus_tracker: Some(focus_tracker.clone()),
2555                                value: value.clone(),
2556                                visual_transformation: None,
2557                                keyboard_type: None,
2558                                ime_action: None,
2559                                enabled: config.enabled,
2560                                read_only: false,
2561                                max_lines: None,
2562                                min_lines: None,
2563                                cursor_color: config
2564                                    .colors
2565                                    .as_ref()
2566                                    .map(|c| c.cursor_color(config.is_error)),
2567                                on_text_layout: None,
2568                            }),
2569                    )
2570                    .semantics(Semantics {
2571                        role: Role::TextField,
2572                        label: None,
2573                        focused: false,
2574                        enabled: true,
2575                        selectable_group: false,
2576                    }),
2577                config.trailing_icon.unwrap_or(Box(Modifier::new())),
2578            )),
2579            // Floating label - absolutely positioned, animates between text-line
2580            // and top-border positions as the field gains content / focus.
2581            // A surface-colored background box hides the border stroke behind the label.
2582            if let Some(lbl) = label_str {
2583                Box(Modifier::new()
2584                    .min_width(200.0)
2585                    .padding_values(PaddingValues {
2586                        left: 20.0,
2587                        right: 20.0,
2588                        top: 0.0,
2589                        bottom: 0.0,
2590                    })
2591                    .absolute()
2592                    .offset(Some(0.0), Some(label_y), None, None))
2593                .child(
2594                    Box(Modifier::new()
2595                        .background(th.surface)
2596                        .padding_values(PaddingValues {
2597                            left: 4.0,
2598                            right: 4.0,
2599                            top: 2.0,
2600                            bottom: 2.0,
2601                        }))
2602                    .child(
2603                        Text(lbl.as_ref().to_string())
2604                            .color(label_color)
2605                            .size(label_size),
2606                    ),
2607                )
2608            } else {
2609                Box(Modifier::new())
2610            },
2611        )),
2612    )
2613}
2614
2615/// Configuration for a filled M3 [`TextField`].
2616#[derive(Clone)]
2617pub struct TextFieldConfig {
2618    pub label: Option<String>,
2619    pub placeholder: Option<String>,
2620    pub leading_icon: Option<View>,
2621    pub trailing_icon: Option<View>,
2622    pub single_line: bool,
2623    pub is_error: bool,
2624    pub enabled: bool,
2625    pub on_submit: Option<Rc<dyn Fn(String)>>,
2626    pub colors: Option<TextFieldColors>,
2627}
2628
2629impl Default for TextFieldConfig {
2630    fn default() -> Self {
2631        Self {
2632            label: None,
2633            placeholder: None,
2634            leading_icon: None,
2635            trailing_icon: None,
2636            single_line: true,
2637            is_error: false,
2638            enabled: true,
2639            on_submit: None,
2640            colors: None,
2641        }
2642    }
2643}
2644
2645/// M3 Filled Text Field with floating label, leading/trailing icons, error state,
2646/// and a bottom indicator line. (Equivalent to Compose Material3's `TextField`.)
2647///
2648/// The label floats up when `value` is non-empty or when the field is focused.
2649/// Container: `SurfaceContainerHighest` bg, top-rounded corners (4dp), flat bottom.
2650/// Indicator: always visible, 1dp (unfocused) / 2dp (focused/error), animated color+thickness.
2651pub fn TextField(
2652    modifier: Modifier,
2653    value: String,
2654    on_value_change: impl Fn(String) + 'static,
2655    config: TextFieldConfig,
2656) -> View {
2657    let th = theme();
2658    let label_str: Option<Rc<str>> = config.label.map(Rc::from);
2659    let has_label = label_str.is_some();
2660
2661    let anim_key = match &label_str {
2662        Some(l) => format!("tf_{}", &l[..l.len().min(32)]),
2663        None => "tf_nolabel".into(),
2664    };
2665
2666    let focus_tracker: Rc<Cell<bool>> =
2667        remember_with_key(format!("tf_focus_{}", anim_key), || Cell::new(false));
2668    let is_focused = focus_tracker.get();
2669    let should_float = !value.is_empty() || is_focused;
2670
2671    let float_t = animate_f32(
2672        anim_key.clone(),
2673        if should_float { 1.0 } else { 0.0 },
2674        th.motion.color,
2675    );
2676
2677    // Resolve colors from config.colors if set, otherwise use legacy theme-based approach
2678    let (indicator_color, label_color, container_bg) = if let Some(ref tc) = config.colors {
2679        let enf = config.enabled && is_focused;
2680        let ind = tc.indicator_color(config.enabled, config.is_error, enf);
2681        let lb = tc.label_color(config.enabled, config.is_error, enf);
2682        let bg = tc.container_color(config.enabled, config.is_error, enf);
2683        (ind, lb, bg)
2684    } else {
2685        let ind = if config.is_error {
2686            th.error
2687        } else if float_t > 0.5 {
2688            th.primary
2689        } else {
2690            th.on_surface_variant
2691        };
2692        let lb = if config.is_error {
2693            th.error
2694        } else if float_t > 0.5 {
2695            th.primary
2696        } else {
2697            th.on_surface_variant
2698        };
2699        let bg = if config.enabled {
2700            th.surface_container_highest
2701        } else {
2702            th.on_surface
2703                .with_alpha_f32(0.04)
2704                .composite_over(th.surface)
2705        };
2706        (ind, lb, bg)
2707    };
2708
2709    let label_size = 16.0 - 4.0 * float_t;
2710    let label_y = 16.0 - 20.0 * float_t;
2711
2712    let tf_placeholder = if has_label {
2713        String::new()
2714    } else {
2715        config.placeholder.unwrap_or_default()
2716    };
2717
2718    let indicator_active = config.is_error || (config.enabled && is_focused);
2719    let indicator_target_w = if indicator_active { 2.0 } else { 1.0 };
2720    let indicator_w = animate_f32(
2721        format!("tf_ind_w_{}", anim_key),
2722        indicator_target_w,
2723        th.motion.color,
2724    );
2725
2726    Box(modifier
2727        .clip_rounded(th.shapes.extra_small)
2728        .background(container_bg))
2729    .child(
2730        Stack(Modifier::new().fill_max_size()).child((
2731            // Bottom indicator line — full width, clipped by container shape
2732            Box(Modifier::new()
2733                .fill_max_size()
2734                .align_items(AlignItems::FlexEnd))
2735            .child(Box(Modifier::new()
2736                .fill_max_width()
2737                .height(indicator_w)
2738                .background(indicator_color))),
2739            // Input row
2740            Row(Modifier::new()
2741                .fill_max_size()
2742                .padding_values(PaddingValues {
2743                    left: 16.0,
2744                    right: 16.0,
2745                    top: 16.0,
2746                    bottom: 10.0,
2747                })
2748                .align_items(AlignItems::Center))
2749            .child((
2750                config.leading_icon.unwrap_or(Box(Modifier::new())),
2751                View::new(0, ViewKind::Box)
2752                    .modifier(
2753                        Modifier::new()
2754                            .flex_grow(1.0)
2755                            .padding_values(PaddingValues {
2756                                left: 8.0,
2757                                right: 8.0,
2758                                top: 0.0,
2759                                bottom: 0.0,
2760                            })
2761                            .text_input(TextInputConfig {
2762                                hint: tf_placeholder,
2763                                multiline: !config.single_line,
2764                                on_change: Some(Rc::new(on_value_change) as _),
2765                                on_submit: config.on_submit.clone().map(|f| {
2766                                    let f = f.clone();
2767                                    Rc::new(move |s| f(s)) as Rc<dyn Fn(String)>
2768                                }),
2769                                focus_tracker: Some(focus_tracker.clone()),
2770                                value: value.clone(),
2771                                visual_transformation: None,
2772                                keyboard_type: None,
2773                                ime_action: None,
2774                                enabled: config.enabled,
2775                                read_only: false,
2776                                max_lines: None,
2777                                min_lines: None,
2778                                cursor_color: config
2779                                    .colors
2780                                    .as_ref()
2781                                    .map(|c| c.cursor_color(config.is_error)),
2782                                on_text_layout: None,
2783                            }),
2784                    )
2785                    .semantics(Semantics {
2786                        role: Role::TextField,
2787                        label: None,
2788                        focused: false,
2789                        enabled: true,
2790                        selectable_group: false,
2791                    }),
2792                config.trailing_icon.unwrap_or(Box(Modifier::new())),
2793            )),
2794            // Floating label
2795            if let Some(lbl) = label_str {
2796                Box(Modifier::new()
2797                    .min_width(200.0)
2798                    .padding_values(PaddingValues {
2799                        left: 20.0,
2800                        right: 20.0,
2801                        top: 0.0,
2802                        bottom: 0.0,
2803                    })
2804                    .absolute()
2805                    .offset(Some(0.0), Some(label_y), None, None))
2806                .child(
2807                    Box(Modifier::new()
2808                        .background(th.surface_container_highest)
2809                        .padding_values(PaddingValues {
2810                            left: 4.0,
2811                            right: 4.0,
2812                            top: 2.0,
2813                            bottom: 2.0,
2814                        }))
2815                    .child(
2816                        Text(lbl.as_ref().to_string())
2817                            .color(label_color)
2818                            .size(label_size),
2819                    ),
2820                )
2821            } else {
2822                Box(Modifier::new())
2823            },
2824        )),
2825    )
2826}
2827
2828/// Configuration for [`Checkbox`].
2829#[derive(Clone, Debug)]
2830pub struct CheckboxConfig {
2831    pub modifier: Modifier,
2832    /// When false, the checkbox renders disabled colors and does not respond to clicks.
2833    pub enabled: bool,
2834    pub checked_color: Color,
2835    pub unchecked_color: Color,
2836    pub checkmark_color: Color,
2837    /// Border color when checked. Default: same as `checked_color`.
2838    pub checked_border_color: Color,
2839    /// Border color when unchecked. Default: same as `unchecked_color`.
2840    pub unchecked_border_color: Color,
2841    pub disabled_checked_box_color: Color,
2842    pub disabled_unchecked_box_color: Color,
2843    pub disabled_indeterminate_box_color: Color,
2844    pub disabled_checkmark_color: Color,
2845    pub disabled_checked_border_color: Color,
2846    pub disabled_unchecked_border_color: Color,
2847    pub disabled_indeterminate_border_color: Color,
2848    pub state_colors: StateColors,
2849}
2850
2851impl Default for CheckboxConfig {
2852    fn default() -> Self {
2853        Self {
2854            modifier: Modifier::new(),
2855            enabled: true,
2856            checked_color: CheckboxDefaults::checked_color(),
2857            unchecked_color: CheckboxDefaults::unchecked_color(),
2858            checkmark_color: CheckboxDefaults::checkmark_color(),
2859            checked_border_color: CheckboxDefaults::checked_color(),
2860            unchecked_border_color: CheckboxDefaults::unchecked_color(),
2861            disabled_checked_box_color: CheckboxDefaults::disabled_checked_box_color(),
2862            disabled_unchecked_box_color: Color::TRANSPARENT,
2863            disabled_indeterminate_box_color: CheckboxDefaults::disabled_checked_box_color(),
2864            disabled_checkmark_color: CheckboxDefaults::disabled_checkmark_color(),
2865            disabled_checked_border_color: CheckboxDefaults::disabled_checked_box_color(),
2866            disabled_unchecked_border_color: CheckboxDefaults::disabled_unchecked_border_color(),
2867            disabled_indeterminate_border_color: CheckboxDefaults::disabled_checked_box_color(),
2868            state_colors: CheckboxDefaults::state_colors_default(),
2869        }
2870    }
2871}
2872
2873/// M3 Checkbox.
2874/// Renders a 40dp touch-target with an 18dp check box inside.
2875/// Fill, border, and check mark animate with 100ms FastOutSlowIn.
2876static CHECKBOX_COUNTER: AtomicU64 = AtomicU64::new(0);
2877pub fn Checkbox(checked: bool, on_change: impl Fn(bool) + 'static, config: CheckboxConfig) -> View {
2878    let th = theme();
2879    let sz = CheckboxDefaults::BOX_SIZE;
2880
2881    let id = remember(|| CHECKBOX_COUNTER.fetch_add(1, Ordering::Relaxed));
2882    let spec = th.motion.color_fast;
2883
2884    let is_enabled = config.enabled;
2885
2886    let fill = animate_color(
2887        format!("cb_fill_{}", id),
2888        if !is_enabled {
2889            if checked {
2890                config.disabled_checked_box_color
2891            } else {
2892                config.disabled_unchecked_box_color
2893            }
2894        } else if checked {
2895            config.checked_color
2896        } else {
2897            Color::TRANSPARENT
2898        },
2899        spec,
2900    );
2901    let bd_w = animate_f32(
2902        format!("cb_bw_{}", id),
2903        if !is_enabled && checked {
2904            0.0
2905        } else if !is_enabled {
2906            CheckboxDefaults::STROKE_WIDTH
2907        } else if checked {
2908            0.0
2909        } else {
2910            CheckboxDefaults::STROKE_WIDTH
2911        },
2912        spec,
2913    );
2914    let bd = animate_color(
2915        format!("cb_bd_{}", id),
2916        if !is_enabled {
2917            if checked {
2918                config.disabled_checked_border_color
2919            } else {
2920                config.disabled_unchecked_border_color
2921            }
2922        } else if checked {
2923            Color::TRANSPARENT
2924        } else {
2925            config.unchecked_border_color
2926        },
2927        spec,
2928    );
2929    let check_alpha = animate_f32(
2930        format!("cb_ca_{}", id),
2931        if checked { 1.0 } else { 0.0 },
2932        spec,
2933    );
2934    let check_col = if !is_enabled {
2935        config.disabled_checkmark_color
2936    } else {
2937        config.checkmark_color
2938    };
2939
2940    let cb = move |_| {
2941        if config.enabled {
2942            on_change(!checked)
2943        }
2944    };
2945
2946    Box(Modifier::new()
2947        .width(CheckboxDefaults::TOUCH_TARGET_SIZE)
2948        .height(CheckboxDefaults::TOUCH_TARGET_SIZE)
2949        .padding(0.0)
2950        .clip_rounded(20.0)
2951        .background(Color::TRANSPARENT)
2952        .state_colors(config.state_colors)
2953        .clickable()
2954        .align_items(AlignItems::Center)
2955        .justify_content(JustifyContent::Center)
2956        .on_pointer_down(cb)
2957        .then(config.modifier))
2958    .child(
2959        Box(Modifier::new()
2960            .size(sz, sz)
2961            .background(fill)
2962            .border(bd_w, bd, CheckboxDefaults::CORNER_RADIUS)
2963            .clip_rounded(CheckboxDefaults::CORNER_RADIUS)
2964            .align_items(AlignItems::Center)
2965            .justify_content(JustifyContent::Center))
2966        .child(if check_alpha > 0.01 {
2967            Box(Modifier::new().alpha(check_alpha)).child(
2968                Icon(Symbol::new("done", '\u{E876}'))
2969                    .color(check_col)
2970                    .size(CheckboxDefaults::CHECK_ICON_SIZE),
2971            )
2972        } else {
2973            Box(Modifier::new())
2974        }),
2975    )
2976}
2977
2978/// Three-state value for [`TriStateCheckbox`].
2979#[derive(Clone, Copy, Debug, PartialEq)]
2980pub enum TriState {
2981    Checked,
2982    Unchecked,
2983    Indeterminate,
2984}
2985
2986/// M3 Tri-State Checkbox - cycles through Checked → Indeterminate → Unchecked.
2987/// Indeterminate shows a dash instead of a checkmark.
2988pub fn TriStateCheckbox(
2989    state: TriState,
2990    on_change: impl Fn(TriState) + 'static,
2991    config: CheckboxConfig,
2992) -> View {
2993    let th = theme();
2994    let sz = CheckboxDefaults::BOX_SIZE;
2995
2996    let id = remember(|| CHECKBOX_COUNTER.fetch_add(1, Ordering::Relaxed));
2997    let spec = th.motion.color_fast;
2998
2999    let is_checked = state == TriState::Checked;
3000    let is_indeterminate = state == TriState::Indeterminate;
3001    let has_fill = is_checked || is_indeterminate;
3002    let is_enabled = config.enabled;
3003
3004    let fill = animate_color(
3005        format!("tc_fill_{}", id),
3006        if !is_enabled {
3007            if has_fill {
3008                config.disabled_indeterminate_box_color
3009            } else {
3010                config.disabled_unchecked_box_color
3011            }
3012        } else if has_fill {
3013            config.checked_color
3014        } else {
3015            Color::TRANSPARENT
3016        },
3017        spec,
3018    );
3019    let bd_w = animate_f32(
3020        format!("tc_bw_{}", id),
3021        if !is_enabled {
3022            if has_fill {
3023                0.0
3024            } else {
3025                CheckboxDefaults::STROKE_WIDTH
3026            }
3027        } else if has_fill {
3028            0.0
3029        } else {
3030            CheckboxDefaults::STROKE_WIDTH
3031        },
3032        spec,
3033    );
3034    let bd = animate_color(
3035        format!("tc_bd_{}", id),
3036        if !is_enabled {
3037            if has_fill {
3038                config.disabled_indeterminate_border_color
3039            } else {
3040                config.disabled_unchecked_border_color
3041            }
3042        } else if has_fill {
3043            Color::TRANSPARENT
3044        } else {
3045            config.unchecked_border_color
3046        },
3047        spec,
3048    );
3049    let symbol_alpha = animate_f32(
3050        format!("tc_sa_{}", id),
3051        if has_fill { 1.0 } else { 0.0 },
3052        spec,
3053    );
3054    let symbol_col = if !is_enabled {
3055        config.disabled_checkmark_color
3056    } else {
3057        config.checkmark_color
3058    };
3059
3060    Box(Modifier::new()
3061        .width(CheckboxDefaults::TOUCH_TARGET_SIZE)
3062        .height(CheckboxDefaults::TOUCH_TARGET_SIZE)
3063        .padding(0.0)
3064        .clip_rounded(20.0)
3065        .background(Color::TRANSPARENT)
3066        .clickable()
3067        .align_items(AlignItems::Center)
3068        .justify_content(JustifyContent::Center)
3069        .on_pointer_down(move |_| {
3070            if is_enabled {
3071                on_change(match state {
3072                    TriState::Checked => TriState::Unchecked,
3073                    TriState::Indeterminate => TriState::Checked,
3074                    TriState::Unchecked => TriState::Checked,
3075                })
3076            }
3077        })
3078        .then(config.modifier))
3079    .child(
3080        Box(Modifier::new()
3081            .size(sz, sz)
3082            .background(fill)
3083            .border(bd_w, bd, CheckboxDefaults::CORNER_RADIUS)
3084            .clip_rounded(CheckboxDefaults::CORNER_RADIUS)
3085            .align_items(AlignItems::Center)
3086            .justify_content(JustifyContent::Center))
3087        .child(if symbol_alpha > 0.01 {
3088            Box(Modifier::new().alpha(symbol_alpha)).child(if is_indeterminate {
3089                // Dash for indeterminate
3090                Box(Modifier::new()
3091                    .width(10.0)
3092                    .height(2.0)
3093                    .background(symbol_col)
3094                    .clip_rounded(1.0))
3095            } else {
3096                Icon(Symbol::new("done", '\u{E876}'))
3097                    .color(symbol_col)
3098                    .size(CheckboxDefaults::CHECK_ICON_SIZE)
3099            })
3100        } else {
3101            Box(Modifier::new())
3102        }),
3103    )
3104}
3105
3106/// Configuration for [`RadioButton`].
3107#[derive(Clone, Debug)]
3108pub struct RadioButtonConfig {
3109    pub modifier: Modifier,
3110    /// When false, renders disabled colors and does not respond to clicks.
3111    pub enabled: bool,
3112    pub selected_color: Color,
3113    pub unselected_color: Color,
3114    pub disabled_selected_color: Color,
3115    pub disabled_unselected_color: Color,
3116    pub state_colors: StateColors,
3117}
3118
3119impl Default for RadioButtonConfig {
3120    fn default() -> Self {
3121        Self {
3122            modifier: Modifier::new(),
3123            enabled: true,
3124            selected_color: RadioButtonDefaults::selected_color(),
3125            unselected_color: RadioButtonDefaults::unselected_color(),
3126            disabled_selected_color: RadioButtonDefaults::disabled_selected_color(),
3127            disabled_unselected_color: RadioButtonDefaults::disabled_unselected_color(),
3128            state_colors: RadioButtonDefaults::state_colors_default(),
3129        }
3130    }
3131}
3132
3133/// M3 RadioButton.
3134/// Renders a 40dp touch-target with a 20dp outer circle + inner dot.
3135/// Ring color animates with 100ms FastOutSlowIn; dot size animates with spring.
3136static RADIO_COUNTER: AtomicU64 = AtomicU64::new(0);
3137pub fn RadioButton(
3138    selected: bool,
3139    on_select: impl Fn() + 'static,
3140    config: RadioButtonConfig,
3141) -> View {
3142    let th = theme();
3143    let d = RadioButtonDefaults::OUTER_RADIUS * 2.0;
3144
3145    let id = remember(|| RADIO_COUNTER.fetch_add(1, Ordering::Relaxed));
3146    let color_spec = th.motion.color_fast;
3147    let spring = th.motion.spring;
3148
3149    let ring_col = animate_color(
3150        format!("rb_ring_{}", id),
3151        if !config.enabled {
3152            if selected {
3153                config.disabled_selected_color
3154            } else {
3155                config.disabled_unselected_color
3156            }
3157        } else if selected {
3158            config.selected_color
3159        } else {
3160            config.unselected_color
3161        },
3162        color_spec,
3163    );
3164    let dot_size = animate_f32(
3165        format!("rb_dot_{}", id),
3166        if selected {
3167            RadioButtonDefaults::DOT_RADIUS * 2.0
3168        } else {
3169            0.0
3170        },
3171        spring,
3172    );
3173    let dot_col = if !config.enabled {
3174        config.disabled_selected_color
3175    } else {
3176        config.selected_color
3177    };
3178
3179    let cb = move |_| {
3180        if config.enabled {
3181            on_select()
3182        }
3183    };
3184
3185    Box(Modifier::new()
3186        .width(RadioButtonDefaults::TOUCH_TARGET_SIZE)
3187        .height(RadioButtonDefaults::TOUCH_TARGET_SIZE)
3188        .padding(0.0)
3189        .clip_rounded(20.0)
3190        .background(Color::TRANSPARENT)
3191        .state_colors(config.state_colors)
3192        .clickable()
3193        .align_items(AlignItems::Center)
3194        .justify_content(JustifyContent::Center)
3195        .on_pointer_down(cb)
3196        .then(config.modifier))
3197    .child(
3198        Box(Modifier::new()
3199            .size(d, d)
3200            .border(RadioButtonDefaults::STROKE_WIDTH, ring_col, d * 0.5)
3201            .clip_rounded(d * 0.5)
3202            .align_items(AlignItems::Center)
3203            .justify_content(JustifyContent::Center))
3204        .child(if dot_size > 0.5 {
3205            Box(Modifier::new()
3206                .size(dot_size, dot_size)
3207                .background(dot_col)
3208                .clip_rounded(dot_size * 0.5))
3209        } else {
3210            Box(Modifier::new())
3211        }),
3212    )
3213}
3214
3215/// Configuration for [`Switch`].
3216#[derive(Clone, Debug)]
3217pub struct SwitchConfig {
3218    pub modifier: Modifier,
3219    /// When false, renders disabled colors and does not respond to clicks.
3220    pub enabled: bool,
3221    pub checked_track_color: Color,
3222    pub unchecked_track_color: Color,
3223    pub checked_thumb_color: Color,
3224    pub unchecked_thumb_color: Color,
3225    /// Icon color for the thumb content when checked. Default: `on_primary`.
3226    pub checked_icon_color: Color,
3227    /// Icon color for the thumb content when unchecked. Default: `outline`.
3228    pub unchecked_icon_color: Color,
3229    /// Border color when checked. Default: transparent.
3230    pub checked_border_color: Color,
3231    /// Border color when unchecked.
3232    pub unchecked_border_color: Color,
3233    pub disabled_checked_thumb_color: Color,
3234    pub disabled_checked_track_color: Color,
3235    pub disabled_checked_border_color: Color,
3236    pub disabled_checked_icon_color: Color,
3237    pub disabled_unchecked_thumb_color: Color,
3238    pub disabled_unchecked_track_color: Color,
3239    pub disabled_unchecked_border_color: Color,
3240    pub disabled_unchecked_icon_color: Color,
3241    pub state_colors: StateColors,
3242}
3243
3244impl Default for SwitchConfig {
3245    fn default() -> Self {
3246        Self {
3247            modifier: Modifier::new(),
3248            enabled: true,
3249            checked_track_color: SwitchDefaults::checked_track_color(),
3250            unchecked_track_color: SwitchDefaults::unchecked_track_color(),
3251            checked_thumb_color: SwitchDefaults::checked_thumb_color(),
3252            unchecked_thumb_color: SwitchDefaults::unchecked_thumb_color(),
3253            checked_icon_color: SwitchDefaults::checked_icon_color(),
3254            unchecked_icon_color: SwitchDefaults::unchecked_icon_color(),
3255            checked_border_color: Color::TRANSPARENT,
3256            unchecked_border_color: SwitchDefaults::unchecked_border_color(),
3257            disabled_checked_thumb_color: SwitchDefaults::disabled_checked_thumb_color(),
3258            disabled_checked_track_color: SwitchDefaults::disabled_checked_track_color(),
3259            disabled_checked_border_color: Color::TRANSPARENT,
3260            disabled_checked_icon_color: SwitchDefaults::disabled_checked_icon_color(),
3261            disabled_unchecked_thumb_color: SwitchDefaults::disabled_unchecked_thumb_color(),
3262            disabled_unchecked_track_color: SwitchDefaults::disabled_unchecked_track_color(),
3263            disabled_unchecked_border_color: SwitchDefaults::disabled_unchecked_border_color(),
3264            disabled_unchecked_icon_color: SwitchDefaults::disabled_unchecked_icon_color(),
3265            state_colors: SwitchDefaults::state_colors_default(),
3266        }
3267    }
3268}
3269
3270/// M3 Switch.
3271/// Renders a pill track with an animated thumb knob.
3272/// Thumb position, size, and colors animate with spring/tween physics.
3273static SWITCH_COUNTER: AtomicU64 = AtomicU64::new(0);
3274pub fn Switch(checked: bool, on_change: impl Fn(bool) + 'static, config: SwitchConfig) -> View {
3275    let th = theme();
3276    let track_w = SwitchDefaults::TRACK_WIDTH;
3277    let track_h = SwitchDefaults::TRACK_HEIGHT;
3278
3279    let id = remember(|| SWITCH_COUNTER.fetch_add(1, Ordering::Relaxed));
3280
3281    let hovered = remember(|| Signal::new(false));
3282    let pressed = remember(|| Signal::new(false));
3283
3284    // Thumb: spring-animated position and size
3285    let thumb_target_pos = if checked {
3286        track_w - SwitchDefaults::THUMB_CHECKED_SIZE - 4.0
3287    } else {
3288        8.0
3289    };
3290    let thumb_target_d = if checked {
3291        SwitchDefaults::THUMB_CHECKED_SIZE
3292    } else {
3293        SwitchDefaults::THUMB_UNCHECKED_SIZE
3294    };
3295    let spring = th.motion.spring;
3296
3297    let thumb_left = animate_f32(format!("sw_pos_{}", id), thumb_target_pos, spring);
3298    let thumb_d = animate_f32(format!("sw_d_{}", id), thumb_target_d, spring);
3299    let thumb_top = (track_h - thumb_d) * 0.5;
3300
3301    let color_spec = th.motion.color_fast;
3302    let is_enabled = config.enabled;
3303
3304    let track_bg = animate_color(
3305        format!("sw_tbg_{}", id),
3306        if !is_enabled {
3307            if checked {
3308                config.disabled_checked_track_color
3309            } else {
3310                config.disabled_unchecked_track_color
3311            }
3312        } else if checked {
3313            config.checked_track_color
3314        } else {
3315            config.unchecked_track_color
3316        },
3317        color_spec,
3318    );
3319    let thumb_bg = animate_color(
3320        format!("sw_tmbg_{}", id),
3321        if !is_enabled {
3322            if checked {
3323                config.disabled_checked_thumb_color
3324            } else {
3325                config.disabled_unchecked_thumb_color
3326            }
3327        } else if checked {
3328            config.checked_thumb_color
3329        } else {
3330            config.unchecked_thumb_color
3331        },
3332        color_spec,
3333    );
3334    let track_border = animate_f32(
3335        format!("sw_tb_{}", id),
3336        if !is_enabled {
3337            if checked { 0.0 } else { 2.0 }
3338        } else if checked {
3339            0.0
3340        } else {
3341            2.0
3342        },
3343        color_spec,
3344    );
3345    let border_color = animate_color(
3346        format!("sw_bc_{}", id),
3347        if !is_enabled {
3348            if checked {
3349                config.disabled_checked_border_color
3350            } else {
3351                config.disabled_unchecked_border_color
3352            }
3353        } else if checked {
3354            config.checked_border_color
3355        } else {
3356            config.unchecked_border_color
3357        },
3358        color_spec,
3359    );
3360
3361    let state_overlay = animate_color(
3362        format!("sw_ol_{}", id),
3363        if !is_enabled {
3364            Color::TRANSPARENT
3365        } else if pressed.get() {
3366            config.state_colors.pressed
3367        } else if hovered.get() {
3368            config.state_colors.hovered
3369        } else {
3370            config.state_colors.default
3371        },
3372        color_spec,
3373    );
3374
3375    Box(Modifier::new()
3376        .size(track_w, track_h)
3377        .padding(0.0)
3378        .clip_rounded(track_h * 0.5)
3379        .background(track_bg)
3380        .border(track_border, border_color, track_h * 0.5)
3381        .clickable()
3382        .on_pointer_enter({
3383            let h = hovered.clone();
3384            move |_| h.set(true)
3385        })
3386        .on_pointer_leave({
3387            let h = hovered.clone();
3388            let p = pressed.clone();
3389            move |_| {
3390                h.set(false);
3391                p.set(false);
3392            }
3393        })
3394        .on_pointer_down({
3395            let p = pressed.clone();
3396            let cb = on_change;
3397            let en = is_enabled;
3398            move |_| {
3399                p.set(true);
3400                if en {
3401                    cb(!checked);
3402                }
3403            }
3404        })
3405        .on_pointer_up({
3406            let p = pressed.clone();
3407            move |_| p.set(false)
3408        })
3409        .then(config.modifier))
3410    .child((
3411        Box(Modifier::new()
3412            .size(thumb_d, thumb_d)
3413            .background(thumb_bg)
3414            .clip_rounded(thumb_d * 0.5)
3415            .absolute()
3416            .offset(Some(thumb_left), Some(thumb_top), None, None)),
3417        Box(Modifier::new()
3418            .size(40.0, 40.0)
3419            .clip_rounded(20.0)
3420            .background(state_overlay)
3421            .absolute()
3422            .offset(
3423                Some(thumb_left + thumb_d * 0.5 - 20.0),
3424                Some(track_h * 0.5 - 20.0),
3425                None,
3426                None,
3427            )),
3428    ))
3429}
3430
3431/// Configuration for [`Slider`] and [`RangeSlider`].
3432#[derive(Clone, Debug)]
3433pub struct SliderConfig {
3434    pub modifier: Modifier,
3435    /// When false, renders disabled colors and does not respond to input.
3436    pub enabled: bool,
3437    pub active_track_color: Color,
3438    pub inactive_track_color: Color,
3439    pub thumb_color: Color,
3440    pub active_tick_color: Color,
3441    pub inactive_tick_color: Color,
3442    pub disabled_thumb_color: Color,
3443    pub disabled_active_track_color: Color,
3444    pub disabled_inactive_track_color: Color,
3445    pub disabled_active_tick_color: Color,
3446    pub disabled_inactive_tick_color: Color,
3447    pub state_colors: StateColors,
3448}
3449
3450impl Default for SliderConfig {
3451    fn default() -> Self {
3452        Self {
3453            modifier: Modifier::new(),
3454            enabled: true,
3455            active_track_color: SliderDefaults::active_track_color(),
3456            inactive_track_color: SliderDefaults::inactive_track_color(),
3457            thumb_color: SliderDefaults::thumb_color(),
3458            active_tick_color: SliderDefaults::active_tick_color(),
3459            inactive_tick_color: SliderDefaults::inactive_tick_color(),
3460            disabled_thumb_color: SliderDefaults::disabled_thumb_color(),
3461            disabled_active_track_color: SliderDefaults::disabled_active_track_color(),
3462            disabled_inactive_track_color: SliderDefaults::disabled_inactive_track_color(),
3463            disabled_active_tick_color: SliderDefaults::disabled_active_tick_color(),
3464            disabled_inactive_tick_color: SliderDefaults::disabled_inactive_tick_color(),
3465            state_colors: SliderDefaults::state_colors_default(),
3466        }
3467    }
3468}
3469
3470static SLIDER_COUNTER: AtomicU64 = AtomicU64::new(0);
3471
3472fn snap_step(v: f32, min: f32, max: f32, step: Option<f32>) -> f32 {
3473    let v = v.clamp(min, max);
3474    if let Some(s) = step.filter(|s| *s > 0.0) {
3475        let t = ((v - min) / s).round();
3476        (min + t * s).clamp(min, max)
3477    } else {
3478        v
3479    }
3480}
3481
3482fn value_from_x(x: f32, rect: Rect, min: f32, max: f32, step: Option<f32>) -> f32 {
3483    let w = rect.w.max(1.0);
3484    let t = ((x - rect.x) / w).clamp(0.0, 1.0);
3485    let v = min + t * (max - min);
3486    snap_step(v, min, max, step)
3487}
3488
3489pub fn Slider(
3490    value: f32,
3491    range: (f32, f32),
3492    step: Option<f32>,
3493    on_change: impl Fn(f32) + 'static,
3494    config: SliderConfig,
3495) -> View {
3496    let id = *remember(|| SLIDER_COUNTER.fetch_add(1, Ordering::Relaxed));
3497    let track_rect = remember_state_with_key(format!("ms_rect_{}", id), || Rect::default());
3498    let drag_active = remember_state_with_key(format!("ms_da_{}", id), || false);
3499    let hovered = remember(|| Signal::new(false));
3500
3501    let track_rect_p = track_rect.clone();
3502    let drag_active_p = drag_active.clone();
3503    let hovered_sig = hovered.clone();
3504    let sc = config.state_colors;
3505
3506    let min = range.0;
3507    let max = range.1;
3508    let oc = Rc::new(on_change);
3509    let range_size = (max - min).max(1e-6);
3510    let t = ((value - min) / range_size).clamp(0.0, 1.0);
3511
3512    let tick_frac: Vec<f32> = if let Some(s) = step {
3513        let n = ((max - min) / s.max(1e-6)).round() as usize;
3514        (0..=n).map(|i| i as f32 / n as f32).collect()
3515    } else {
3516        Vec::new()
3517    };
3518
3519    Box(Modifier::new()
3520        .min_width(200.0)
3521        .height(44.0)
3522        .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
3523            let mul_c = |c: Color| {
3524                Color(
3525                    c.0,
3526                    c.1,
3527                    c.2,
3528                    ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
3529                )
3530            };
3531            let track_h = dp_to_px(16.0);
3532            let thumb_w = dp_to_px(4.0);
3533            let thumb_h = dp_to_px(44.0);
3534            let dot_r = dp_to_px(2.0);
3535            let corner = track_h * 0.5;
3536            let gap = thumb_w * 0.5 + dp_to_px(ProgressIndicatorDefaults::SLIDER_THUMB_TRACK_GAP);
3537            let pad = thumb_w * 0.5;
3538            let track_x = rect.x + pad;
3539            let track_w = (rect.w - thumb_w).max(0.0);
3540            let cy = rect.y + rect.h * 0.5;
3541
3542            let kx = if step.is_some() && !tick_frac.is_empty() {
3543                let is_first = (t - tick_frac[0]).abs() < 1e-6;
3544                let is_last = (t - tick_frac[tick_frac.len() - 1]).abs() < 1e-6;
3545                if is_first || is_last {
3546                    track_x + t * track_w
3547                } else {
3548                    track_x + (track_w - track_h) * t + corner
3549                }
3550            } else {
3551                track_x + t * track_w
3552            };
3553
3554            *track_rect_p.borrow_mut() = Rect {
3555                x: track_x,
3556                y: rect.y,
3557                w: track_w,
3558                h: rect.h,
3559            };
3560
3561            let inactive_x = track_x.max(kx + gap);
3562            let inactive_w = (track_x + track_w - inactive_x).max(0.0);
3563            if inactive_w > 0.0 {
3564                scene.nodes.push(SceneNode::Rect {
3565                    rect: Rect {
3566                        x: inactive_x,
3567                        y: cy - track_h * 0.5,
3568                        w: inactive_w,
3569                        h: track_h,
3570                    },
3571                    brush: Brush::Solid(mul_c(config.inactive_track_color)),
3572                    radius: [corner; 4],
3573                });
3574            }
3575            let fill_w = (kx - gap - track_x).max(0.0);
3576            if fill_w > 0.0 {
3577                scene.nodes.push(SceneNode::Rect {
3578                    rect: Rect {
3579                        x: track_x,
3580                        y: cy - track_h * 0.5,
3581                        w: fill_w,
3582                        h: track_h,
3583                    },
3584                    brush: Brush::Solid(mul_c(config.active_track_color)),
3585                    radius: [corner; 4],
3586                });
3587            }
3588            let tick_start = track_x + corner;
3589            let tick_end = track_x + track_w - corner;
3590            for (i, &tf) in tick_frac.iter().enumerate() {
3591                let tx = tick_start + tf * (tick_end - tick_start);
3592                // skip ticks that fall on the stop indicator (last)
3593                if i == tick_frac.len() - 1 {
3594                    continue;
3595                }
3596                if tx >= kx - gap && tx <= kx + gap {
3597                    continue;
3598                }
3599                let on_active = tx <= kx - gap;
3600                scene.nodes.push(SceneNode::Ellipse {
3601                    rect: Rect {
3602                        x: tx - dot_r,
3603                        y: cy - dot_r,
3604                        w: dot_r * 2.0,
3605                        h: dot_r * 2.0,
3606                    },
3607                    brush: Brush::Solid(mul_c(if on_active {
3608                        config.active_tick_color
3609                    } else {
3610                        config.inactive_tick_color
3611                    })),
3612                });
3613            }
3614            let sx = track_x + track_w - corner;
3615            scene.nodes.push(SceneNode::Ellipse {
3616                rect: Rect {
3617                    x: sx - dot_r,
3618                    y: cy - dot_r,
3619                    w: dot_r * 2.0,
3620                    h: dot_r * 2.0,
3621                },
3622                brush: Brush::Solid(mul_c(config.inactive_tick_color)),
3623            });
3624            let da = *drag_active_p.borrow();
3625            let hv = hovered_sig.get();
3626            let tw = if da { thumb_w * 0.5 } else { thumb_w };
3627            scene.nodes.push(SceneNode::Rect {
3628                rect: Rect {
3629                    x: kx - tw * 0.5,
3630                    y: cy - thumb_h * 0.5,
3631                    w: tw,
3632                    h: thumb_h,
3633                },
3634                brush: Brush::Solid(mul_c(config.thumb_color)),
3635                radius: [tw * 0.5; 4],
3636            });
3637            let sc_target = if da {
3638                sc.pressed
3639            } else if hv {
3640                sc.hovered
3641            } else {
3642                sc.default
3643            };
3644            if sc_target.3 > 0 {
3645                scene.nodes.push(SceneNode::Rect {
3646                    rect: Rect {
3647                        x: kx - tw * 0.5,
3648                        y: cy - thumb_h * 0.5,
3649                        w: tw,
3650                        h: thumb_h,
3651                    },
3652                    brush: Brush::Solid(mul_c(sc_target)),
3653                    radius: [tw * 0.5; 4],
3654                });
3655            }
3656        })
3657        .on_pointer_enter({
3658            let h = hovered.clone();
3659            move |_pe: PointerEvent| h.set(true)
3660        })
3661        .on_pointer_leave({
3662            let h = hovered.clone();
3663            move |_pe: PointerEvent| h.set(false)
3664        })
3665        .on_pointer_down({
3666            let oc = oc.clone();
3667            let track_rect = track_rect.clone();
3668            let drag_active = drag_active.clone();
3669            move |pe: PointerEvent| {
3670                *drag_active.borrow_mut() = true;
3671                let r = *track_rect.borrow();
3672                (oc)(value_from_x(pe.position.x, r, min, max, step));
3673            }
3674        })
3675        .on_pointer_move({
3676            let oc = oc.clone();
3677            let track_rect = track_rect.clone();
3678            let drag_active = drag_active.clone();
3679            move |pe: PointerEvent| {
3680                if !*drag_active.borrow() {
3681                    return;
3682                }
3683                let r = *track_rect.borrow();
3684                (oc)(value_from_x(pe.position.x, r, min, max, step));
3685            }
3686        })
3687        .on_pointer_up(move |_pe: PointerEvent| {
3688            *drag_active.borrow_mut() = false;
3689        })
3690        .on_scroll({
3691            let oc = oc.clone();
3692            move |d: Vec2| -> Vec2 {
3693                let dir = if d.y < -0.5 {
3694                    1
3695                } else if d.y > 0.5 {
3696                    -1
3697                } else {
3698                    0
3699                };
3700                if dir == 0 {
3701                    return d;
3702                }
3703                let step_val = step.unwrap_or(1.0).max(1e-6);
3704                let new_val = snap_step(value + (dir as f32) * step_val, min, max, step);
3705                if (new_val - value).abs() > 1e-6 {
3706                    (oc)(new_val);
3707                    Vec2 { x: d.x, y: 0.0 }
3708                } else {
3709                    d
3710                }
3711            }
3712        })
3713        .then(config.modifier))
3714    .semantics(Semantics {
3715        role: Role::Slider,
3716        label: None,
3717        focused: false,
3718        enabled: true,
3719        selectable_group: false,
3720    })
3721}
3722
3723pub fn RangeSlider(
3724    start: f32,
3725    end: f32,
3726    range: (f32, f32),
3727    step: Option<f32>,
3728    on_change: impl Fn(f32, f32) + 'static,
3729    config: SliderConfig,
3730) -> View {
3731    let id = *remember(|| SLIDER_COUNTER.fetch_add(1, Ordering::Relaxed));
3732    let track_rect = remember_state_with_key(format!("mrs_rect_{}", id), || Rect::default());
3733    let drag_active = remember_state_with_key(format!("mrs_da_{}", id), || false);
3734    let active_thumb = remember_state_with_key(format!("mrs_at_{}", id), || false);
3735    let hovered = remember(|| Signal::new(false));
3736
3737    let min = range.0;
3738    let max = range.1;
3739    let oc = Rc::new(on_change);
3740    let range_size = (max - min).max(1e-6);
3741    let t0 = ((start - min) / range_size).clamp(0.0, 1.0);
3742    let t1 = ((end - min) / range_size).clamp(0.0, 1.0);
3743    let sc = config.state_colors;
3744    let is_enabled = config.enabled;
3745
3746    let act_trk = if !is_enabled {
3747        config.disabled_active_track_color
3748    } else {
3749        config.active_track_color
3750    };
3751    let inact_trk = if !is_enabled {
3752        config.disabled_inactive_track_color
3753    } else {
3754        config.inactive_track_color
3755    };
3756    let act_tick = if !is_enabled {
3757        config.disabled_active_tick_color
3758    } else {
3759        config.active_tick_color
3760    };
3761    let inact_tick = if !is_enabled {
3762        config.disabled_inactive_tick_color
3763    } else {
3764        config.inactive_tick_color
3765    };
3766    let thumb_col = if !is_enabled {
3767        config.disabled_thumb_color
3768    } else {
3769        config.thumb_color
3770    };
3771
3772    let tick_frac: Vec<f32> = if let Some(s) = step {
3773        let n = ((max - min) / s.max(1e-6)).round() as usize;
3774        (0..=n).map(|i| i as f32 / n as f32).collect()
3775    } else {
3776        Vec::new()
3777    };
3778
3779    let track_rect_p = track_rect.clone();
3780    let drag_active_p = drag_active.clone();
3781    let active_thumb_p = active_thumb.clone();
3782    let hovered_sig = hovered.clone();
3783
3784    Box(Modifier::new()
3785        .min_width(200.0)
3786        .height(44.0)
3787        .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
3788            let mul_c = |c: Color| {
3789                Color(
3790                    c.0,
3791                    c.1,
3792                    c.2,
3793                    ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
3794                )
3795            };
3796            let track_h = dp_to_px(16.0);
3797            let thumb_w = dp_to_px(4.0);
3798            let thumb_h = dp_to_px(44.0);
3799            let dot_r = dp_to_px(2.0);
3800            let corner = track_h * 0.5;
3801            let gap = thumb_w * 0.5 + dp_to_px(ProgressIndicatorDefaults::SLIDER_THUMB_TRACK_GAP);
3802            let pad = thumb_w * 0.5;
3803            let track_x = rect.x + pad;
3804            let track_w = (rect.w - thumb_w).max(0.0);
3805            let cy = rect.y + rect.h * 0.5;
3806
3807            let thumb_pos = |tf: f32, fracs: &[f32]| {
3808                if step.is_some() && !fracs.is_empty() {
3809                    let is_first = (tf - fracs[0]).abs() < 1e-6;
3810                    let is_last = (tf - fracs[fracs.len() - 1]).abs() < 1e-6;
3811                    if is_first || is_last {
3812                        track_x + tf * track_w
3813                    } else {
3814                        track_x + (track_w - track_h) * tf + corner
3815                    }
3816                } else {
3817                    track_x + tf * track_w
3818                }
3819            };
3820            let k0 = thumb_pos(t0, &tick_frac);
3821            let k1 = thumb_pos(t1, &tick_frac);
3822            let active_l = k0.min(k1);
3823            let active_r = k0.max(k1);
3824
3825            *track_rect_p.borrow_mut() = Rect {
3826                x: track_x,
3827                y: rect.y,
3828                w: track_w,
3829                h: rect.h,
3830            };
3831
3832            let linactive_w = (active_l - gap - track_x).max(0.0);
3833            if linactive_w > 0.0 {
3834                scene.nodes.push(SceneNode::Rect {
3835                    rect: Rect {
3836                        x: track_x,
3837                        y: cy - track_h * 0.5,
3838                        w: linactive_w,
3839                        h: track_h,
3840                    },
3841                    brush: Brush::Solid(mul_c(inact_trk)),
3842                    radius: [corner; 4],
3843                });
3844            }
3845            let rinactive_x = (active_r + gap).min(track_x + track_w);
3846            let rinactive_w = (track_x + track_w - rinactive_x).max(0.0);
3847            if rinactive_w > 0.0 {
3848                scene.nodes.push(SceneNode::Rect {
3849                    rect: Rect {
3850                        x: rinactive_x,
3851                        y: cy - track_h * 0.5,
3852                        w: rinactive_w,
3853                        h: track_h,
3854                    },
3855                    brush: Brush::Solid(mul_c(inact_trk)),
3856                    radius: [corner; 4],
3857                });
3858            }
3859            let active_w = (active_r - gap - (active_l + gap)).max(0.0);
3860            if active_w > 0.0 {
3861                scene.nodes.push(SceneNode::Rect {
3862                    rect: Rect {
3863                        x: active_l + gap,
3864                        y: cy - track_h * 0.5,
3865                        w: active_w,
3866                        h: track_h,
3867                    },
3868                    brush: Brush::Solid(mul_c(act_trk)),
3869                    radius: [corner; 4],
3870                });
3871            }
3872            let tick_start = track_x + corner;
3873            let tick_end = track_x + track_w - corner;
3874            for (i, &tf) in tick_frac.iter().enumerate() {
3875                let tx = tick_start + tf * (tick_end - tick_start);
3876                // skip ticks that fall on the stop indicators (first and last)
3877                if i == 0 || i == tick_frac.len() - 1 {
3878                    continue;
3879                }
3880                let in_lgap = tx >= active_l - gap && tx <= active_l + gap;
3881                let in_rgap = tx >= active_r - gap && tx <= active_r + gap;
3882                if in_lgap || in_rgap {
3883                    continue;
3884                }
3885                let on_active = tx >= active_l + gap && tx <= active_r - gap;
3886                scene.nodes.push(SceneNode::Ellipse {
3887                    rect: Rect {
3888                        x: tx - dot_r,
3889                        y: cy - dot_r,
3890                        w: dot_r * 2.0,
3891                        h: dot_r * 2.0,
3892                    },
3893                    brush: Brush::Solid(mul_c(if on_active { act_tick } else { inact_tick })),
3894                });
3895            }
3896            // Stop indicators at both track ends (always drawn, matching Compose)
3897            let sx0 = track_x + corner;
3898            scene.nodes.push(SceneNode::Ellipse {
3899                rect: Rect {
3900                    x: sx0 - dot_r,
3901                    y: cy - dot_r,
3902                    w: dot_r * 2.0,
3903                    h: dot_r * 2.0,
3904                },
3905                brush: Brush::Solid(mul_c(inact_tick)),
3906            });
3907            let sx = track_x + track_w - corner;
3908            scene.nodes.push(SceneNode::Ellipse {
3909                rect: Rect {
3910                    x: sx - dot_r,
3911                    y: cy - dot_r,
3912                    w: dot_r * 2.0,
3913                    h: dot_r * 2.0,
3914                },
3915                brush: Brush::Solid(mul_c(inact_tick)),
3916            });
3917            let da = *drag_active_p.borrow();
3918            let at = *active_thumb_p.borrow();
3919            let hv = hovered_sig.get();
3920            let thumbs = [k0, k1];
3921            for (idx, &kx) in thumbs.iter().enumerate() {
3922                let is_active = da && (if idx == 0 { !at } else { at });
3923                let tw = if is_active { thumb_w * 0.5 } else { thumb_w };
3924                scene.nodes.push(SceneNode::Rect {
3925                    rect: Rect {
3926                        x: kx - tw * 0.5,
3927                        y: cy - thumb_h * 0.5,
3928                        w: tw,
3929                        h: thumb_h,
3930                    },
3931                    brush: Brush::Solid(mul_c(thumb_col)),
3932                    radius: [tw * 0.5; 4],
3933                });
3934                let sc_target = if !is_enabled {
3935                    Color::TRANSPARENT
3936                } else if is_active {
3937                    sc.pressed
3938                } else if hv {
3939                    sc.hovered
3940                } else {
3941                    sc.default
3942                };
3943                if sc_target.3 > 0 {
3944                    scene.nodes.push(SceneNode::Rect {
3945                        rect: Rect {
3946                            x: kx - tw * 0.5,
3947                            y: cy - thumb_h * 0.5,
3948                            w: tw,
3949                            h: thumb_h,
3950                        },
3951                        brush: Brush::Solid(mul_c(sc_target)),
3952                        radius: [tw * 0.5; 4],
3953                    });
3954                }
3955            }
3956        })
3957        .on_pointer_enter({
3958            let h = hovered.clone();
3959            let en = is_enabled;
3960            move |_pe: PointerEvent| {
3961                if en {
3962                    h.set(true);
3963                }
3964            }
3965        })
3966        .on_pointer_leave({
3967            let h = hovered.clone();
3968            move |_pe: PointerEvent| h.set(false)
3969        })
3970        .on_pointer_down({
3971            let oc = oc.clone();
3972            let track_rect = track_rect.clone();
3973            let drag_active = drag_active.clone();
3974            let active_thumb = active_thumb.clone();
3975            let en = is_enabled;
3976            move |pe: PointerEvent| {
3977                if !en {
3978                    return;
3979                }
3980                *drag_active.borrow_mut() = true;
3981                let r = *track_rect.borrow();
3982                let v = value_from_x(pe.position.x, r, min, max, step);
3983                let use_end = (v - end).abs() < (v - start).abs();
3984                *active_thumb.borrow_mut() = use_end;
3985                let (a, b) = if use_end {
3986                    (start, v.max(start))
3987                } else {
3988                    (v.min(end), end)
3989                };
3990                (oc)(a, b);
3991            }
3992        })
3993        .on_pointer_move({
3994            let oc = oc.clone();
3995            let track_rect = track_rect.clone();
3996            let drag_active = drag_active.clone();
3997            let active_thumb = active_thumb.clone();
3998            move |pe: PointerEvent| {
3999                if !*drag_active.borrow() {
4000                    return;
4001                }
4002                let r = *track_rect.borrow();
4003                let v = value_from_x(pe.position.x, r, min, max, step);
4004                let use_end = *active_thumb.borrow();
4005                let (a, b) = if use_end {
4006                    (start, v.max(start))
4007                } else {
4008                    (v.min(end), end)
4009                };
4010                (oc)(a, b);
4011            }
4012        })
4013        .on_pointer_up({
4014            let drag_active = drag_active.clone();
4015            let active_thumb = active_thumb.clone();
4016            move |_pe: PointerEvent| {
4017                *drag_active.borrow_mut() = false;
4018                *active_thumb.borrow_mut() = false;
4019            }
4020        })
4021        .on_scroll({
4022            let oc = oc.clone();
4023            let active_thumb = active_thumb.clone();
4024            let en = is_enabled;
4025            move |d: Vec2| -> Vec2 {
4026                if !en {
4027                    return d;
4028                }
4029                let dir = if d.y < -0.5 {
4030                    1
4031                } else if d.y > 0.5 {
4032                    -1
4033                } else {
4034                    0
4035                };
4036                if dir == 0 {
4037                    return d;
4038                }
4039                let step_val = step.unwrap_or(1.0).max(1e-6);
4040                let use_end = *active_thumb.borrow();
4041                let (mut a, mut b) = (start, end);
4042                if use_end {
4043                    b = snap_step(end + (dir as f32) * step_val, min, max, step).max(a);
4044                } else {
4045                    a = snap_step(start + (dir as f32) * step_val, min, max, step).min(b);
4046                }
4047                if (a - start).abs() > 1e-6 || (b - end).abs() > 1e-6 {
4048                    (oc)(a, b);
4049                    Vec2 { x: d.x, y: 0.0 }
4050                } else {
4051                    d
4052                }
4053            }
4054        })
4055        .then(config.modifier))
4056    .semantics(Semantics {
4057        role: Role::Slider,
4058        label: None,
4059        focused: false,
4060        enabled: is_enabled,
4061        selectable_group: false,
4062    })
4063}
4064
4065/// Configuration for [`Card`].
4066#[derive(Clone, Debug)]
4067pub struct CardConfig {
4068    pub modifier: Modifier,
4069    /// When false, renders disabled colors and does not respond to clicks.
4070    pub enabled: bool,
4071    pub container_color: Color,
4072    pub content_color: Color,
4073    pub disabled_container_color: Color,
4074    pub disabled_content_color: Color,
4075    pub shape_radius: f32,
4076    pub tonal_elevation: f32,
4077    pub state_elevation: Option<StateElevation>,
4078    pub border: Option<(f32, Color)>,
4079}
4080
4081impl Default for CardConfig {
4082    fn default() -> Self {
4083        Self {
4084            modifier: Modifier::new(),
4085            enabled: true,
4086            container_color: CardDefaults::filled_container_color(),
4087            content_color: CardDefaults::filled_content_color(),
4088            disabled_container_color: CardDefaults::disabled_container_color(),
4089            disabled_content_color: CardDefaults::disabled_content_color(),
4090            shape_radius: CardDefaults::SHAPE_RADIUS,
4091            tonal_elevation: CardDefaults::ELEVATION,
4092            state_elevation: None,
4093            border: None,
4094        }
4095    }
4096}
4097
4098/// M3 Card - a configurable container surface.
4099pub fn Card(config: CardConfig, content: impl FnOnce() -> View) -> View {
4100    let bg = if !config.enabled {
4101        config.disabled_container_color
4102    } else {
4103        config.container_color
4104    };
4105    let fg = if !config.enabled {
4106        config.disabled_content_color
4107    } else {
4108        config.content_color
4109    };
4110    let mut m = Modifier::new()
4111        .background(bg)
4112        .clip_rounded(config.shape_radius)
4113        .then(config.modifier);
4114    if let Some((w, c)) = config.border {
4115        m = m.border(w, c, config.shape_radius);
4116    }
4117    if let Some(se) = config.state_elevation {
4118        m = m.state_elevation(se);
4119    } else if config.tonal_elevation > 0.0 {
4120        m = m.state_elevation(StateElevation {
4121            default: config.tonal_elevation,
4122            hovered: config.tonal_elevation,
4123            pressed: config.tonal_elevation,
4124            disabled: 0.0,
4125        });
4126    }
4127    Box(m).color(fg).child(content())
4128}
4129
4130/// M3 Elevated Card - card with elevation.
4131pub fn ElevatedCard(modifier: Modifier, content: View) -> View {
4132    let th = theme();
4133    Card(
4134        CardConfig {
4135            modifier,
4136            container_color: CardDefaults::elevated_container_color(),
4137            state_elevation: Some(StateElevation {
4138                default: th.elevation.level1,
4139                hovered: th.elevation.level2,
4140                pressed: th.elevation.level3,
4141                disabled: 0.0,
4142            }),
4143            ..Default::default()
4144        },
4145        || Column(Modifier::new().fill_max_size()).child(content),
4146    )
4147}
4148
4149/// M3 Outlined Card - card with border outline.
4150pub fn OutlinedCard(modifier: Modifier, content: View) -> View {
4151    Card(
4152        CardConfig {
4153            modifier,
4154            container_color: CardDefaults::outlined_container_color(),
4155            border: Some((1.0, CardDefaults::outlined_border_color())),
4156            ..Default::default()
4157        },
4158        || Column(Modifier::new().fill_max_size()).child(content),
4159    )
4160}
4161
4162fn card_state_colors(bg: Color) -> StateColors {
4163    let th = theme();
4164    StateColors {
4165        default: Color::TRANSPARENT,
4166        hovered: th.on_surface.with_alpha_f32(0.08).composite_over(bg),
4167        pressed: th.on_surface.with_alpha_f32(0.12).composite_over(bg),
4168        disabled: th.on_surface.with_alpha_f32(0.12).composite_over(bg),
4169    }
4170}
4171
4172fn clickable_card_impl(
4173    on_click: impl Fn() + 'static,
4174    modifier: Modifier,
4175    bg: Color,
4176    shape_radius: f32,
4177    config: CardConfig,
4178    content: impl FnOnce() -> View,
4179) -> View {
4180    let m = modifier
4181        .state_colors(card_state_colors(bg))
4182        .clickable()
4183        .on_pointer_down({
4184            let cb = on_click;
4185            let en = config.enabled;
4186            move |_| {
4187                if en {
4188                    cb();
4189                }
4190            }
4191        });
4192    Card(
4193        CardConfig {
4194            modifier: m,
4195            enabled: config.enabled,
4196            container_color: bg,
4197            content_color: config.content_color,
4198            disabled_container_color: config.disabled_container_color,
4199            disabled_content_color: config.disabled_content_color,
4200            shape_radius,
4201            border: config.border,
4202            state_elevation: config.state_elevation,
4203            tonal_elevation: config.tonal_elevation,
4204        },
4205        || Column(Modifier::new().fill_max_size()).child(content()),
4206    )
4207}
4208
4209/// M3 Clickable Filled Card - interactive card with state coloring.
4210pub fn ClickableCard(
4211    on_click: impl Fn() + 'static,
4212    modifier: Modifier,
4213    config: CardConfig,
4214    content: impl FnOnce() -> View,
4215) -> View {
4216    let th = theme();
4217    clickable_card_impl(
4218        on_click,
4219        modifier,
4220        th.surface_container_highest,
4221        th.shapes.medium,
4222        config,
4223        content,
4224    )
4225}
4226
4227/// M3 Clickable Elevated Card - interactive card with elevation.
4228pub fn ClickableElevatedCard(
4229    on_click: impl Fn() + 'static,
4230    modifier: Modifier,
4231    config: CardConfig,
4232    content: impl FnOnce() -> View,
4233) -> View {
4234    let th = theme();
4235    let cfg = CardConfig {
4236        state_elevation: Some(StateElevation {
4237            default: th.elevation.level1,
4238            hovered: th.elevation.level2,
4239            pressed: th.elevation.level3,
4240            disabled: 0.0,
4241        }),
4242        ..config
4243    };
4244    clickable_card_impl(
4245        on_click,
4246        modifier,
4247        th.surface,
4248        th.shapes.medium,
4249        cfg,
4250        content,
4251    )
4252}
4253
4254/// M3 Clickable Outlined Card - interactive card with border.
4255pub fn ClickableOutlinedCard(
4256    on_click: impl Fn() + 'static,
4257    modifier: Modifier,
4258    config: CardConfig,
4259    content: impl FnOnce() -> View,
4260) -> View {
4261    let th = theme();
4262    let cfg = CardConfig {
4263        border: Some((1.0, th.outline_variant)),
4264        ..config
4265    };
4266    clickable_card_impl(
4267        on_click,
4268        modifier,
4269        th.surface,
4270        th.shapes.medium,
4271        cfg,
4272        content,
4273    )
4274}
4275
4276/// Configuration for [`Snackbar`].
4277#[derive(Clone, Debug)]
4278pub struct SnackbarConfig {
4279    pub modifier: Modifier,
4280    pub container_color: Color,
4281    pub content_color: Color,
4282    pub action_color: Color,
4283    pub dismiss_action_content_color: Color,
4284    pub action_on_new_line: bool,
4285    pub shape_radius: f32,
4286    pub min_height: f32,
4287    pub min_width: f32,
4288    pub max_width: f32,
4289}
4290
4291impl Default for SnackbarConfig {
4292    fn default() -> Self {
4293        Self {
4294            modifier: Modifier::new(),
4295            container_color: SnackbarDefaults::container_color(),
4296            content_color: SnackbarDefaults::content_color(),
4297            action_color: SnackbarDefaults::action_color(),
4298            dismiss_action_content_color: SnackbarDefaults::dismiss_action_content_color(),
4299            action_on_new_line: false,
4300            shape_radius: SnackbarDefaults::SHAPE_RADIUS,
4301            min_height: SnackbarDefaults::MIN_HEIGHT,
4302            min_width: SnackbarDefaults::MIN_WIDTH,
4303            max_width: SnackbarDefaults::MAX_WIDTH,
4304        }
4305    }
4306}
4307
4308/// Color slots for chips (both non-selectable and selectable).
4309#[derive(Clone, Copy, Debug)]
4310pub struct ChipColors {
4311    pub container_color: Color,
4312    pub label_color: Color,
4313    pub leading_icon_color: Color,
4314    pub trailing_icon_color: Color,
4315    pub disabled_container_color: Color,
4316    pub disabled_label_color: Color,
4317    pub disabled_leading_icon_color: Color,
4318    pub disabled_trailing_icon_color: Color,
4319    pub selected_container_color: Color,
4320    pub selected_label_color: Color,
4321    pub selected_leading_icon_color: Color,
4322    pub selected_trailing_icon_color: Color,
4323    pub disabled_selected_container_color: Color,
4324}
4325
4326impl ChipColors {
4327    pub fn container(&self, enabled: bool, selected: bool) -> Color {
4328        match (enabled, selected) {
4329            (true, true) => self.selected_container_color,
4330            (true, false) => self.container_color,
4331            (false, true) => self.disabled_selected_container_color,
4332            (false, false) => self.disabled_container_color,
4333        }
4334    }
4335    pub fn label(&self, enabled: bool, selected: bool) -> Color {
4336        if !enabled {
4337            self.disabled_label_color
4338        } else if selected {
4339            self.selected_label_color
4340        } else {
4341            self.label_color
4342        }
4343    }
4344    pub fn leading_icon(&self, enabled: bool, selected: bool) -> Color {
4345        if !enabled {
4346            self.disabled_leading_icon_color
4347        } else if selected {
4348            self.selected_leading_icon_color
4349        } else {
4350            self.leading_icon_color
4351        }
4352    }
4353    pub fn trailing_icon(&self, enabled: bool, selected: bool) -> Color {
4354        if !enabled {
4355            self.disabled_trailing_icon_color
4356        } else if selected {
4357            self.selected_trailing_icon_color
4358        } else {
4359            self.trailing_icon_color
4360        }
4361    }
4362}
4363
4364/// Elevation levels for chips.
4365#[derive(Clone, Copy, Debug)]
4366pub struct ChipElevation {
4367    pub default: f32,
4368    pub hovered: f32,
4369    pub focused: f32,
4370    pub pressed: f32,
4371    pub dragged: f32,
4372    pub disabled: f32,
4373}
4374
4375impl ChipElevation {
4376    pub fn to_state_elevation(&self) -> StateElevation {
4377        StateElevation {
4378            default: self.default,
4379            hovered: self.hovered,
4380            pressed: self.pressed,
4381            disabled: self.disabled,
4382        }
4383    }
4384}
4385
4386impl Default for ChipElevation {
4387    fn default() -> Self {
4388        Self {
4389            default: ChipDefaults::elevation_default(),
4390            hovered: ChipDefaults::elevation_hovered(),
4391            focused: ChipDefaults::elevation_focused(),
4392            pressed: ChipDefaults::elevation_pressed(),
4393            dragged: ChipDefaults::elevation_dragged(),
4394            disabled: ChipDefaults::elevation_disabled(),
4395        }
4396    }
4397}
4398
4399/// Configuration for chips.
4400#[derive(Clone, Debug)]
4401pub struct ChipConfig {
4402    pub modifier: Modifier,
4403    pub enabled: bool,
4404    pub colors: ChipColors,
4405    pub elevation: ChipElevation,
4406    pub border_width: f32,
4407    pub border_color: Color,
4408    pub selected_border_color: Color,
4409    pub disabled_border_color: Color,
4410    pub disabled_selected_border_color: Color,
4411    pub shape_radius: f32,
4412    pub horizontal_padding: f32,
4413}
4414
4415impl Default for ChipConfig {
4416    fn default() -> Self {
4417        Self {
4418            modifier: Modifier::new(),
4419            enabled: true,
4420            colors: ChipColors {
4421                container_color: ChipDefaults::container_color(),
4422                label_color: ChipDefaults::label_color(),
4423                leading_icon_color: ChipDefaults::leading_icon_color(),
4424                trailing_icon_color: ChipDefaults::trailing_icon_color(),
4425                disabled_container_color: ChipDefaults::disabled_container_color(),
4426                disabled_label_color: ChipDefaults::disabled_label_color(),
4427                disabled_leading_icon_color: ChipDefaults::disabled_leading_icon_color(),
4428                disabled_trailing_icon_color: ChipDefaults::disabled_trailing_icon_color(),
4429                selected_container_color: ChipDefaults::selected_container_color(),
4430                selected_label_color: ChipDefaults::selected_label_color(),
4431                selected_leading_icon_color: ChipDefaults::selected_leading_icon_color(),
4432                selected_trailing_icon_color: ChipDefaults::selected_trailing_icon_color(),
4433                disabled_selected_container_color: ChipDefaults::disabled_selected_container_color(
4434                ),
4435            },
4436            elevation: ChipElevation::default(),
4437            border_width: ChipDefaults::BORDER_WIDTH,
4438            border_color: ChipDefaults::border_color(),
4439            selected_border_color: ChipDefaults::selected_border_color(),
4440            disabled_border_color: ChipDefaults::disabled_border_color(),
4441            disabled_selected_border_color: ChipDefaults::disabled_selected_border_color(),
4442            shape_radius: ChipDefaults::SHAPE_RADIUS,
4443            horizontal_padding: ChipDefaults::HORIZONTAL_PADDING,
4444        }
4445    }
4446}
4447
4448/// M3 Assist Chip - a chip for triggering actions.
4449pub fn AssistChip(
4450    on_click: impl Fn() + 'static,
4451    label: View,
4452    leading_icon: Option<View>,
4453    trailing_icon: Option<View>,
4454    config: ChipConfig,
4455) -> View {
4456    let th = theme();
4457    let is_enabled = config.enabled;
4458    let colors = &config.colors;
4459    let bg = colors.container(is_enabled, false);
4460    let label_color = colors.label(is_enabled, false);
4461    let leading_color = colors.leading_icon(is_enabled, false);
4462    let trailing_color = colors.trailing_icon(is_enabled, false);
4463    let border = if is_enabled {
4464        config.border_color
4465    } else {
4466        config.disabled_border_color
4467    };
4468    let shape = config.shape_radius;
4469
4470    let mut m = Modifier::new()
4471        .state_colors(StateColors {
4472            default: Color::TRANSPARENT,
4473            hovered: th.on_surface.with_alpha_f32(0.08),
4474            pressed: th.on_surface.with_alpha_f32(0.12),
4475            disabled: Color::TRANSPARENT,
4476        })
4477        .padding_values(PaddingValues {
4478            left: config.horizontal_padding,
4479            right: config.horizontal_padding,
4480            top: 8.0,
4481            bottom: 8.0,
4482        })
4483        .background(bg)
4484        .clip_rounded(shape)
4485        .then(config.modifier);
4486
4487    if config.border_width > 0.0 && border != Color::TRANSPARENT {
4488        m = m.border(config.border_width, border, shape);
4489    }
4490
4491    if is_enabled {
4492        m = m.clickable().on_pointer_down(move |_| on_click());
4493    }
4494
4495    Box(m).child(
4496        Row(Modifier::new().align_items(AlignItems::Center)).child((
4497            leading_icon
4498                .map(|v| {
4499                    Box(Modifier::new().padding_values(PaddingValues {
4500                        left: 0.0,
4501                        right: 8.0,
4502                        top: 0.0,
4503                        bottom: 0.0,
4504                    }))
4505                    .child(with_content_color(leading_color, move || v))
4506                })
4507                .unwrap_or(Box(Modifier::new())),
4508            with_content_color(label_color, move || label),
4509            trailing_icon
4510                .map(|v| {
4511                    Box(Modifier::new().padding_values(PaddingValues {
4512                        left: 8.0,
4513                        right: 0.0,
4514                        top: 0.0,
4515                        bottom: 0.0,
4516                    }))
4517                    .child(with_content_color(trailing_color, move || v))
4518                })
4519                .unwrap_or(Box(Modifier::new())),
4520        )),
4521    )
4522}
4523
4524/// M3 Elevated Assist Chip - like [`AssistChip`] but with elevated container.
4525pub fn ElevatedAssistChip(
4526    on_click: impl Fn() + 'static,
4527    label: View,
4528    leading_icon: Option<View>,
4529    trailing_icon: Option<View>,
4530    config: ChipConfig,
4531) -> View {
4532    let th = theme();
4533    let is_enabled = config.enabled;
4534    let colors = &config.colors;
4535    let bg = colors.container(is_enabled, false);
4536    let label_color = colors.label(is_enabled, false);
4537    let leading_color = colors.leading_icon(is_enabled, false);
4538    let trailing_color = colors.trailing_icon(is_enabled, false);
4539    let shape = config.shape_radius;
4540
4541    let mut m = Modifier::new()
4542        .state_colors(StateColors {
4543            default: Color::TRANSPARENT,
4544            hovered: th.on_surface.with_alpha_f32(0.08),
4545            pressed: th.on_surface.with_alpha_f32(0.12),
4546            disabled: Color::TRANSPARENT,
4547        })
4548        .state_elevation(config.elevation.to_state_elevation())
4549        .padding_values(PaddingValues {
4550            left: config.horizontal_padding,
4551            right: config.horizontal_padding,
4552            top: 8.0,
4553            bottom: 8.0,
4554        })
4555        .background(bg)
4556        .clip_rounded(shape)
4557        .then(config.modifier);
4558
4559    if is_enabled {
4560        m = m.clickable().on_pointer_down(move |_| on_click());
4561    }
4562
4563    Box(m).child(
4564        Row(Modifier::new().align_items(AlignItems::Center)).child((
4565            leading_icon
4566                .map(|v| {
4567                    Box(Modifier::new().padding_values(PaddingValues {
4568                        left: 0.0,
4569                        right: 8.0,
4570                        top: 0.0,
4571                        bottom: 0.0,
4572                    }))
4573                    .child(with_content_color(leading_color, move || v))
4574                })
4575                .unwrap_or(Box(Modifier::new())),
4576            with_content_color(label_color, move || label),
4577            trailing_icon
4578                .map(|v| {
4579                    Box(Modifier::new().padding_values(PaddingValues {
4580                        left: 8.0,
4581                        right: 0.0,
4582                        top: 0.0,
4583                        bottom: 0.0,
4584                    }))
4585                    .child(with_content_color(trailing_color, move || v))
4586                })
4587                .unwrap_or(Box(Modifier::new())),
4588        )),
4589    )
4590}
4591
4592/// Configuration for [`NavigationBar`].
4593#[derive(Clone, Debug)]
4594pub struct NavigationBarConfig {
4595    pub modifier: Modifier,
4596    pub container_color: Color,
4597    pub content_color: Color,
4598    pub selected_icon_color: Color,
4599    pub selected_text_color: Color,
4600    pub unselected_icon_color: Color,
4601    pub unselected_text_color: Color,
4602    pub indicator_color: Color,
4603    pub height: f32,
4604    pub tonal_elevation: f32,
4605    pub indicator_opacity: f32,
4606    pub indicator_radius: f32,
4607    pub item_spacing: f32,
4608    pub indicator_width: f32,
4609    pub indicator_height: f32,
4610}
4611
4612impl Default for NavigationBarConfig {
4613    fn default() -> Self {
4614        Self {
4615            modifier: Modifier::new(),
4616            container_color: NavigationBarDefaults::container_color(),
4617            content_color: NavigationBarDefaults::content_color(),
4618            selected_icon_color: NavigationBarDefaults::selected_icon_color(),
4619            selected_text_color: NavigationBarDefaults::selected_text_color(),
4620            unselected_icon_color: NavigationBarDefaults::unselected_icon_color(),
4621            unselected_text_color: NavigationBarDefaults::unselected_text_color(),
4622            indicator_color: NavigationBarDefaults::indicator_color(),
4623            height: NavigationBarDefaults::HEIGHT,
4624            tonal_elevation: NavigationBarDefaults::TONAL_ELEVATION,
4625            indicator_opacity: NavigationBarDefaults::ITEM_ACTIVE_INDICATOR_OPACITY,
4626            indicator_radius: NavigationBarDefaults::INDICATOR_RADIUS,
4627            item_spacing: NavigationBarDefaults::ITEM_SPACING,
4628            indicator_width: NavigationBarDefaults::ACTIVE_INDICATOR_WIDTH,
4629            indicator_height: NavigationBarDefaults::ACTIVE_INDICATOR_HEIGHT,
4630        }
4631    }
4632}
4633
4634/// Configuration for [`NavigationRail`].
4635#[derive(Clone, Debug)]
4636pub struct NavigationRailConfig {
4637    pub modifier: Modifier,
4638    pub container_color: Color,
4639    pub selected_icon_color: Color,
4640    pub selected_text_color: Color,
4641    pub unselected_icon_color: Color,
4642    pub unselected_text_color: Color,
4643    pub indicator_color: Color,
4644    pub width: f32,
4645    pub item_radius: f32,
4646    pub indicator_opacity: f32,
4647    pub item_spacing: f32,
4648    pub indicator_width: f32,
4649    pub indicator_height: f32,
4650}
4651
4652impl Default for NavigationRailConfig {
4653    fn default() -> Self {
4654        Self {
4655            modifier: Modifier::new(),
4656            container_color: NavigationRailDefaults::container_color(),
4657            selected_icon_color: NavigationRailDefaults::selected_icon_color(),
4658            selected_text_color: NavigationRailDefaults::selected_text_color(),
4659            unselected_icon_color: NavigationRailDefaults::unselected_icon_color(),
4660            unselected_text_color: NavigationRailDefaults::unselected_text_color(),
4661            indicator_color: NavigationRailDefaults::indicator_color(),
4662            width: NavigationRailDefaults::WIDTH,
4663            item_radius: NavigationRailDefaults::ITEM_RADIUS,
4664            indicator_opacity: NavigationRailDefaults::ITEM_ACTIVE_INDICATOR_OPACITY,
4665            item_spacing: NavigationRailDefaults::ITEM_SPACING,
4666            indicator_width: NavigationRailDefaults::ACTIVE_INDICATOR_WIDTH,
4667            indicator_height: NavigationRailDefaults::ACTIVE_INDICATOR_HEIGHT,
4668        }
4669    }
4670}
4671
4672/// Configuration for [`Scaffold`].
4673#[derive(Clone, Debug)]
4674pub struct ScaffoldConfig {
4675    pub modifier: Modifier,
4676    pub container_color: Color,
4677    pub top_bar_height: f32,
4678    pub bottom_bar_height: f32,
4679    pub fab_margin: f32,
4680}
4681
4682impl Default for ScaffoldConfig {
4683    fn default() -> Self {
4684        Self {
4685            modifier: Modifier::new(),
4686            container_color: ScaffoldDefaults::container_color(),
4687            top_bar_height: ScaffoldDefaults::TOP_BAR_HEIGHT,
4688            bottom_bar_height: ScaffoldDefaults::BOTTOM_BAR_HEIGHT,
4689            fab_margin: ScaffoldDefaults::FAB_MARGIN,
4690        }
4691    }
4692}
4693
4694/// Configuration for [`NavigationDrawer`].
4695#[derive(Clone, Debug)]
4696pub struct NavigationDrawerConfig {
4697    pub modifier: Modifier,
4698    pub container_color: Color,
4699    pub content_color: Color,
4700    pub scrim_color: Color,
4701    pub tonal_elevation: f32,
4702    pub width: f32,
4703    pub shape_radius: f32,
4704}
4705
4706impl Default for NavigationDrawerConfig {
4707    fn default() -> Self {
4708        Self {
4709            modifier: Modifier::new(),
4710            container_color: NavigationDrawerDefaults::container_color(),
4711            content_color: NavigationDrawerDefaults::content_color(),
4712            scrim_color: NavigationDrawerDefaults::scrim_color(),
4713            tonal_elevation: NavigationDrawerDefaults::TONAL_ELEVATION,
4714            width: NavigationDrawerDefaults::WIDTH,
4715            shape_radius: NavigationDrawerDefaults::SHAPE_RADIUS,
4716        }
4717    }
4718}
4719
4720/// Configuration for [`BottomSheet`] / `ModalBottomSheet`.
4721#[derive(Clone, Debug)]
4722pub struct BottomSheetConfig {
4723    pub modifier: Modifier,
4724    pub container_color: Color,
4725    pub content_color: Color,
4726    pub scrim_color: Color,
4727    pub tonal_elevation: f32,
4728    pub drag_handle_color: Color,
4729    pub shape_radius: f32,
4730    pub max_width: f32,
4731    pub drag_handle_width: f32,
4732    pub drag_handle_height: f32,
4733    pub peek_height: f32,
4734}
4735
4736impl Default for BottomSheetConfig {
4737    fn default() -> Self {
4738        Self {
4739            modifier: Modifier::new(),
4740            container_color: BottomSheetDefaults::container_color(),
4741            content_color: BottomSheetDefaults::content_color(),
4742            scrim_color: BottomSheetDefaults::scrim_color(),
4743            tonal_elevation: BottomSheetDefaults::TONAL_ELEVATION,
4744            drag_handle_color: BottomSheetDefaults::drag_handle_color(),
4745            shape_radius: BottomSheetDefaults::SHAPE_RADIUS,
4746            max_width: BottomSheetDefaults::MAX_WIDTH,
4747            drag_handle_width: BottomSheetDefaults::DRAG_HANDLE_WIDTH,
4748            drag_handle_height: BottomSheetDefaults::DRAG_HANDLE_HEIGHT,
4749            peek_height: BottomSheetDefaults::PEEK_HEIGHT,
4750        }
4751    }
4752}
4753
4754/// Configuration for [`SearchBar`].
4755#[derive(Clone, Debug)]
4756pub struct SearchBarConfig {
4757    pub modifier: Modifier,
4758    pub container_color: Color,
4759    pub active_container_color: Color,
4760    pub content_color: Color,
4761    pub placeholder_color: Color,
4762    pub height: f32,
4763    pub expanded_width: f32,
4764    pub collapsed_width: f32,
4765}
4766
4767impl Default for SearchBarConfig {
4768    fn default() -> Self {
4769        Self {
4770            modifier: Modifier::new(),
4771            container_color: SearchBarDefaults::container_color(),
4772            active_container_color: SearchBarDefaults::active_container_color(),
4773            content_color: SearchBarDefaults::content_color(),
4774            placeholder_color: SearchBarDefaults::placeholder_color(),
4775            height: SearchBarDefaults::HEIGHT,
4776            expanded_width: SearchBarDefaults::EXPANDED_WIDTH,
4777            collapsed_width: SearchBarDefaults::COLLAPSED_WIDTH,
4778        }
4779    }
4780}
4781
4782/// Configuration for [`DropdownMenu`].
4783#[derive(Clone, Debug)]
4784pub struct DropdownMenuConfig {
4785    pub modifier: Modifier,
4786    pub container_color: Color,
4787    pub item_text_color: Color,
4788    pub disabled_item_text_color: Color,
4789    pub divider_color: Color,
4790    pub min_width: f32,
4791    pub item_height: f32,
4792}
4793
4794impl Default for DropdownMenuConfig {
4795    fn default() -> Self {
4796        Self {
4797            modifier: Modifier::new(),
4798            container_color: DropdownMenuDefaults::container_color(),
4799            item_text_color: DropdownMenuDefaults::item_text_color(),
4800            disabled_item_text_color: DropdownMenuDefaults::disabled_item_text_color(),
4801            divider_color: DropdownMenuDefaults::divider_color(),
4802            min_width: DropdownMenuDefaults::MIN_WIDTH,
4803            item_height: DropdownMenuDefaults::ITEM_HEIGHT,
4804        }
4805    }
4806}
4807
4808/// Configuration for tooltip.
4809#[derive(Clone, Debug)]
4810pub struct TooltipConfig {
4811    pub modifier: Modifier,
4812    pub container_color: Color,
4813    pub content_color: Color,
4814    pub offset_y: f32,
4815    pub horizontal_padding: f32,
4816    pub vertical_padding: f32,
4817}
4818
4819impl Default for TooltipConfig {
4820    fn default() -> Self {
4821        Self {
4822            modifier: Modifier::new(),
4823            container_color: TooltipDefaults::container_color(),
4824            content_color: TooltipDefaults::content_color(),
4825            offset_y: TooltipDefaults::OFFSET_Y,
4826            horizontal_padding: TooltipDefaults::HORIZONTAL_PADDING,
4827            vertical_padding: TooltipDefaults::VERTICAL_PADDING,
4828        }
4829    }
4830}
4831
4832/// Configuration for swipe-to-dismiss.
4833#[derive(Clone, Debug)]
4834pub struct SwipeToDismissConfig {
4835    pub modifier: Modifier,
4836    pub dismiss_threshold: f32,
4837    pub dismissed_offset: f32,
4838    pub animation_spec: AnimationSpec,
4839}
4840
4841impl Default for SwipeToDismissConfig {
4842    fn default() -> Self {
4843        Self {
4844            modifier: Modifier::new(),
4845            dismiss_threshold: SwipeToDismissDefaults::DISMISS_THRESHOLD,
4846            dismissed_offset: SwipeToDismissDefaults::DISMISSED_OFFSET,
4847            animation_spec: AnimationSpec::spring_gentle(),
4848        }
4849    }
4850}
4851
4852/// Configuration for pull-to-refresh.
4853#[derive(Clone, Debug)]
4854pub struct PullToRefreshConfig {
4855    pub modifier: Modifier,
4856    pub indicator_color: Color,
4857    pub threshold: f32,
4858}
4859
4860impl Default for PullToRefreshConfig {
4861    fn default() -> Self {
4862        Self {
4863            modifier: Modifier::new(),
4864            indicator_color: PullToRefreshDefaults::indicator_color(),
4865            threshold: PullToRefreshDefaults::THRESHOLD,
4866        }
4867    }
4868}