cranpose_liquid/widgets/
card.rs1use crate::theme::{liquid_colors, liquid_typography};
4use cranpose_animation::animateColorAsState;
5use cranpose_macros::composable;
6use cranpose_ui::rememberMutableInteractionSource;
7use cranpose_ui::text::{SpanStyle, TextStyle};
8use cranpose_ui::widgets::{Box, BoxSpec, Column, ColumnSpec, Text};
9use cranpose_ui::Modifier;
10use cranpose_ui_graphics::{Brush, Color, CornerRadii};
11use std::cell::RefCell;
12use std::rc::Rc;
13
14use crate::motion::LiquidMotion;
15
16const CARD_RADIUS: f32 = 20.0;
17
18#[composable]
20#[allow(non_snake_case)]
21pub fn LiquidCard(modifier: Modifier, content: impl FnMut() + 'static) {
22 let colors = liquid_colors();
23 let surface = colors.surface;
24 let shadow_alpha = if colors.is_dark { 0.35 } else { 0.07 };
25 let base = Modifier::empty()
26 .drop_shadow(
27 cranpose_ui_graphics::LayerShape::Rounded(
28 cranpose_ui_graphics::RoundedCornerShape::uniform(CARD_RADIUS),
29 ),
30 move |scope| {
31 scope.radius = 14.0;
32 scope.offset.y = 3.0;
33 scope.color = Color::BLACK.with_alpha(shadow_alpha);
34 },
35 )
36 .rounded_corners(CARD_RADIUS)
37 .draw_behind(move |scope| {
38 scope.draw_round_rect(Brush::solid(surface), CornerRadii::uniform(CARD_RADIUS));
39 });
40 Box(base.then(modifier), BoxSpec::default(), content);
41}
42
43#[composable]
45#[allow(non_snake_case)]
46pub fn LiquidListSection(
47 modifier: Modifier,
48 header: impl Into<String>,
49 content: impl FnMut() + 'static,
50) {
51 let colors = liquid_colors();
52 let typography = liquid_typography();
53 let header = header.into();
54 let content = Rc::new(RefCell::new(content));
55 Column(modifier, ColumnSpec::default(), move || {
56 if !header.is_empty() {
57 let style = TextStyle {
58 span_style: SpanStyle {
59 color: Some(colors.secondary_label),
60 ..typography.footnote.span_style.clone()
61 },
62 ..typography.footnote.clone()
63 };
64 Text(
65 header.to_uppercase(),
66 Modifier::empty().padding_each(20.0, 0.0, 20.0, 6.0),
67 style,
68 );
69 }
70 let content = Rc::clone(&content);
71 LiquidCard(Modifier::empty().fill_max_width(), move || {
75 let content = Rc::clone(&content);
76 Column(
77 Modifier::empty().fill_max_width(),
78 ColumnSpec::default(),
79 move || {
80 (content.borrow_mut())();
81 },
82 );
83 });
84 });
85}
86
87#[derive(Clone, Debug, Default, PartialEq)]
89pub struct LiquidListRowSpec {
90 pub separator: bool,
92}
93
94impl LiquidListRowSpec {
95 pub fn with_separator(mut self, separator: bool) -> Self {
96 self.separator = separator;
97 self
98 }
99}
100
101#[composable]
104#[allow(non_snake_case)]
105pub fn LiquidListRow(
106 modifier: Modifier,
107 spec: LiquidListRowSpec,
108 on_click: impl Fn() + 'static,
109 content: impl FnMut() + 'static,
110) {
111 let colors = liquid_colors();
112 let interaction = rememberMutableInteractionSource();
113 let pressed = interaction.collectIsPressedAsState();
114 let wash = animateColorAsState(
115 if pressed.get() {
116 colors.surface_pressed
117 } else {
118 colors.surface_pressed.with_alpha(0.0)
119 },
120 LiquidMotion::snappy(),
121 "row-wash",
122 );
123
124 let separator = spec.separator;
125 let separator_color = colors.separator;
126 let on_click = Rc::new(RefCell::new(on_click));
127 let base = Modifier::empty()
128 .fill_max_width()
129 .press_interaction_source(interaction)
130 .clickable(move |_point| {
131 (on_click.borrow_mut())();
132 })
133 .draw_behind(move |scope| {
134 let size = scope.size();
135 scope.draw_rect(Brush::solid(wash.get()));
136 if separator {
137 scope.draw_rect_at(
138 cranpose_ui_graphics::Rect {
139 x: 16.0,
140 y: size.height - 0.5,
141 width: (size.width - 16.0).max(0.0),
142 height: 0.5,
143 },
144 Brush::solid(separator_color),
145 );
146 }
147 })
148 .padding_symmetric(16.0, 12.0);
149
150 Box(base.then(modifier), BoxSpec::default(), content);
151}