cranpose_liquid/widgets/
card.rs1use std::{cell::RefCell, rc::Rc};
2
3use cranpose_animation::animateColorAsState;
4use cranpose_macros::composable;
5use cranpose_ui::{
6 Modifier, rememberMutableInteractionSource,
7 text::{SpanStyle, TextStyle},
8 widgets::{Box, BoxSpec, Column, ColumnSpec, Text},
9};
10use cranpose_ui_graphics::{Brush, CornerRadii};
11
12use crate::{
13 material::{Glass, GlassDynamics, LiquidModifierExt, LiquidShape},
14 motion::LiquidMotion,
15 theme::{liquid_colors, liquid_typography},
16};
17
18const CARD_RADIUS: f32 = 20.0;
19
20#[composable]
22pub fn Surface(modifier: Modifier, content: impl FnMut() + 'static) {
23 let color = liquid_colors().surface;
24 Box(
25 Modifier::empty()
26 .draw_behind(move |scope| {
27 scope.draw_round_rect(Brush::solid(color), CornerRadii::uniform(12.0));
28 })
29 .then(modifier),
30 BoxSpec::default(),
31 content,
32 );
33}
34
35#[composable]
37pub fn Card(modifier: Modifier, content: impl FnMut() + 'static) {
38 LiquidCard(modifier, content);
39}
40
41#[composable]
44pub fn LiquidCard(modifier: Modifier, content: impl FnMut() + 'static) {
45 let colors = liquid_colors();
46 let surface_glass = colors.surface_glass;
47 let base = Modifier::empty().glass_effect_with(
48 Glass::regular().shape(LiquidShape::RoundedRect(CARD_RADIUS)),
49 move || GlassDynamics {
50 activity: Some(0.0),
51 resting_tint: Some(surface_glass),
52 ..Default::default()
53 },
54 );
55 Box(base.then(modifier), BoxSpec::default(), content);
56}
57
58#[composable]
60pub fn LiquidListSection(
61 modifier: Modifier,
62 header: impl Into<String>,
63 content: impl FnMut() + 'static,
64) {
65 let colors = liquid_colors();
66 let typography = liquid_typography();
67 let header = header.into();
68 let content = Rc::new(RefCell::new(content));
69 Column(modifier, ColumnSpec::default(), move || {
70 if !header.is_empty() {
71 let style = TextStyle {
72 span_style: SpanStyle {
73 color: Some(colors.secondary_label),
74 ..typography.footnote.span_style.clone()
75 },
76 ..typography.footnote.clone()
77 };
78 Text(
79 header.to_uppercase(),
80 Modifier::empty().padding_each(20.0, 0.0, 20.0, 6.0),
81 style,
82 );
83 }
84 let content = Rc::clone(&content);
85 LiquidCard(Modifier::empty().fill_max_width(), move || {
86 let content = Rc::clone(&content);
87 Column(
88 Modifier::empty().fill_max_width(),
89 ColumnSpec::default(),
90 move || {
91 (content.borrow_mut())();
92 },
93 );
94 });
95 });
96}
97
98#[derive(Clone, Debug, Default, PartialEq)]
100pub struct LiquidListRowSpec {
101 pub separator: bool,
103}
104
105impl LiquidListRowSpec {
106 pub fn with_separator(mut self, separator: bool) -> Self {
107 self.separator = separator;
108 self
109 }
110}
111
112#[composable]
115pub fn LiquidListRow(
116 modifier: Modifier,
117 spec: LiquidListRowSpec,
118 on_click: impl Fn() + 'static,
119 content: impl FnMut() + 'static,
120) {
121 let colors = liquid_colors();
122 let interaction = rememberMutableInteractionSource();
123 let pressed = interaction.collectIsPressedAsState();
124 let wash = animateColorAsState(
125 if pressed.get() {
126 colors.surface_pressed
127 } else {
128 colors.surface_pressed.with_alpha(0.0)
129 },
130 LiquidMotion::snappy(),
131 "row-wash",
132 );
133
134 let separator = spec.separator;
135 let separator_color = colors.separator;
136 let on_click = Rc::new(RefCell::new(on_click));
137 let base = Modifier::empty()
138 .fill_max_width()
139 .press_interaction_source(interaction)
140 .clickable(move |_point| {
141 (on_click.borrow_mut())();
142 })
143 .draw_behind(move |scope| {
144 let size = scope.size();
145 scope.draw_rect(Brush::solid(wash.get()));
146 if separator {
147 scope.draw_rect_at(
148 cranpose_ui_graphics::Rect {
149 x: 16.0,
150 y: size.height - 0.5,
151 width: (size.width - 16.0).max(0.0),
152 height: 0.5,
153 },
154 Brush::solid(separator_color),
155 );
156 }
157 })
158 .padding_symmetric(16.0, 12.0);
159
160 Box(base.then(modifier), BoxSpec::default(), content);
161}
162
163#[cfg(test)]
164#[path = "tests/card_tests.rs"]
165mod tests;