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::{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.draw_behind(move |scope| {
26 scope.draw_round_rect(Brush::solid(color), CornerRadii::uniform(12.0));
27 }),
28 BoxSpec::default(),
29 content,
30 );
31}
32
33#[composable]
35pub fn Card(modifier: Modifier, content: impl FnMut() + 'static) {
36 LiquidCard(modifier, content);
37}
38
39#[composable]
42pub fn LiquidCard(modifier: Modifier, content: impl FnMut() + 'static) {
43 let colors = liquid_colors();
44 let base = Modifier::empty().glass_effect(
45 super::glass_surface::card_surface_material(colors.label)
46 .shape(LiquidShape::RoundedRect(CARD_RADIUS)),
47 );
48 Box(modifier.then(base), BoxSpec::default(), content);
49}
50
51#[composable]
53pub fn LiquidListSection(
54 modifier: Modifier,
55 header: impl Into<String>,
56 content: impl FnMut() + 'static,
57) {
58 let colors = liquid_colors();
59 let typography = liquid_typography();
60 let header = header.into();
61 let content = Rc::new(RefCell::new(content));
62 Column(modifier, ColumnSpec::default(), move || {
63 if !header.is_empty() {
64 let style = TextStyle {
65 span_style: SpanStyle {
66 color: Some(colors.secondary_label),
67 ..typography.footnote.span_style.clone()
68 },
69 ..typography.footnote.clone()
70 };
71 Text(
72 header.to_uppercase(),
73 Modifier::empty().padding_each(20.0, 0.0, 20.0, 6.0),
74 style,
75 );
76 }
77 let content = Rc::clone(&content);
78 LiquidCard(Modifier::empty().fill_max_width(), move || {
79 let content = Rc::clone(&content);
80 Column(
81 Modifier::empty().fill_max_width(),
82 ColumnSpec::default(),
83 move || {
84 (content.borrow_mut())();
85 },
86 );
87 });
88 });
89}
90
91#[derive(Clone, Debug, Default, PartialEq)]
93pub struct LiquidListRowSpec {
94 pub separator: bool,
96}
97
98impl LiquidListRowSpec {
99 pub fn with_separator(mut self, separator: bool) -> Self {
100 self.separator = separator;
101 self
102 }
103}
104
105#[composable]
108pub fn LiquidListRow(
109 modifier: Modifier,
110 spec: LiquidListRowSpec,
111 on_click: impl Fn() + 'static,
112 content: impl FnMut() + 'static,
113) {
114 let colors = liquid_colors();
115 let interaction = rememberMutableInteractionSource();
116 let pressed = interaction.collectIsPressedAsState();
117 let wash = animateColorAsState(
118 if pressed.get() {
119 colors.surface_pressed
120 } else {
121 colors.surface_pressed.with_alpha(0.0)
122 },
123 LiquidMotion::snappy(),
124 "row-wash",
125 );
126
127 let separator = spec.separator;
128 let separator_color = colors.separator;
129 let base = Modifier::empty()
130 .fill_max_width()
131 .press_interaction_source(interaction)
132 .clickable(move |_point| {
133 on_click();
134 })
135 .draw_behind(move |scope| {
136 let size = scope.size();
137 scope.draw_rect(Brush::solid(wash.get()));
138 if separator {
139 scope.draw_rect_at(
140 cranpose_ui_graphics::Rect {
141 x: 16.0,
142 y: size.height - 1.0,
143 width: (size.width - 32.0).max(0.0),
144 height: 1.0,
145 },
146 Brush::solid(separator_color),
147 );
148 }
149 })
150 .padding_symmetric(16.0, 12.0);
151
152 Box(modifier.then(base), BoxSpec::default(), content);
153}
154
155#[cfg(test)]
156#[path = "tests/card_tests.rs"]
157mod tests;