Skip to main content

cranpose_liquid/widgets/
chip.rs

1//! Filter chip: a pill that lifts onto glass when selected.
2
3use crate::material::{Glass, LiquidModifierExt};
4use crate::motion::liquid_press_scale;
5use crate::theme::{liquid_colors, liquid_typography};
6use cranpose_animation::animateColorAsState;
7use cranpose_macros::composable;
8use cranpose_services::{default_haptics, HapticFeedback};
9use cranpose_ui::rememberMutableInteractionSource;
10use cranpose_ui::text::FontWeight;
11use cranpose_ui::text::{SpanStyle, TextStyle};
12use cranpose_ui::widgets::{Box, BoxSpec, Text};
13use cranpose_ui::Modifier;
14use cranpose_ui_graphics::{Brush, CornerRadii};
15use cranpose_ui_layout::Alignment;
16use std::cell::RefCell;
17use std::rc::Rc;
18
19use crate::motion::LiquidMotion;
20
21/// A selectable filter pill (the Library "All / Receipts" chips). Selected
22/// chips render on glass; unselected ones sit on the fill color.
23#[composable]
24#[allow(non_snake_case)]
25pub fn LiquidChip(
26    modifier: Modifier,
27    selected: bool,
28    on_click: impl Fn() + 'static,
29    label: impl Into<String>,
30) {
31    let colors = liquid_colors();
32    let typography = liquid_typography();
33    let interaction = rememberMutableInteractionSource();
34    let (pressed_modifier, _pressed, content_alpha) =
35        liquid_press_scale(Modifier::empty(), interaction.clone(), 1.18);
36
37    let label_color = animateColorAsState(
38        if selected {
39            colors.accent
40        } else {
41            colors.secondary_label
42        },
43        LiquidMotion::smooth(),
44        "chip-label",
45    );
46
47    let mut base = Modifier::empty();
48    if selected {
49        base = base.glass_effect(Glass::regular().adaptive_frost(colors.accent, 0.65));
50    } else {
51        let fill = colors.fill;
52        base = base.clip_to_bounds().draw_behind(move |scope| {
53            scope.draw_round_rect(Brush::solid(fill), CornerRadii::uniform(999.0));
54        });
55    }
56
57    let on_click = Rc::new(RefCell::new(on_click));
58    let base = base
59        .press_interaction_source(interaction)
60        .clickable(move |_point| {
61            default_haptics().perform(HapticFeedback::Selection);
62            (on_click.borrow_mut())();
63        })
64        .padding_symmetric(14.0, 7.0);
65
66    let label = label.into();
67    let chip = base.then(modifier);
68    Box(pressed_modifier, BoxSpec::default(), move || {
69        let label = label.clone();
70        let typography = typography.clone();
71        Box(
72            chip.clone(),
73            BoxSpec::default().content_alignment(Alignment::CENTER),
74            move || {
75                let label = label.clone();
76                let style = TextStyle {
77                    span_style: SpanStyle {
78                        color: Some(label_color.get()),
79                        font_weight: Some(if selected {
80                            FontWeight::SEMI_BOLD
81                        } else {
82                            FontWeight::MEDIUM
83                        }),
84                        ..typography.subheadline.span_style.clone()
85                    },
86                    ..typography.subheadline.clone()
87                };
88                let content_layer =
89                    Modifier::empty().graphics_layer(move || cranpose_ui_graphics::GraphicsLayer {
90                        alpha: content_alpha.get().clamp(0.0, 1.0),
91                        ..Default::default()
92                    });
93                Text(label, content_layer, style);
94            },
95        );
96    });
97}