repose_material/material3/
mod.rs1#![allow(non_snake_case)]
2
3mod components;
4pub use components::*;
5
6use std::rc::Rc;
7
8use repose_core::*;
9use repose_ui::{
10 Box, Column, Row, Spacer, Stack, Surface, Text, TextStyle, ViewExt, anim::animate_f32,
11 overlay::SnackbarAction,
12};
13
14pub fn AlertDialog(
15 visible: bool,
16 on_dismiss: impl Fn() + 'static,
17 title: View,
18 text: View,
19 confirm_button: View,
20 dismiss_button: Option<View>,
21) -> View {
22 if !visible {
23 return Box(Modifier::new());
24 }
25
26 Stack(Modifier::new().fill_max_size()).child((
27 Box(Modifier::new()
29 .fill_max_size()
30 .background(Color::from_hex("#000000AA"))
31 .clickable()
32 .on_pointer_down(move |_| on_dismiss())),
33 Surface(
35 Modifier::new()
36 .size(280.0, 200.0)
37 .background(theme().surface)
38 .clip_rounded(28.0)
39 .padding(24.0),
40 Column(Modifier::new()).child((
41 title,
42 Box(Modifier::new().size(1.0, 16.0)),
43 text,
44 Spacer(),
45 Row(Modifier::new()).child((
46 dismiss_button.unwrap_or(Box(Modifier::new())),
47 Spacer(),
48 confirm_button,
49 )),
50 )),
51 ),
52 ))
53}
54
55pub fn BottomSheet(
56 visible: bool,
57 on_dismiss: impl Fn() + 'static,
58 modifier: Modifier,
59 content: View,
60) -> View {
61 let offset = animate_f32(
62 "sheet_offset",
63 if visible { 0.0 } else { 800.0 },
64 AnimationSpec::spring_gentle(),
65 );
66
67 Stack(Modifier::new().fill_max_size()).child((
68 if visible {
70 Box(Modifier::new()
71 .fill_max_size()
72 .background(Color::from_hex("#00000055"))
73 .on_pointer_down(move |_| on_dismiss()))
74 } else {
75 Box(Modifier::new())
76 },
77 Box(modifier
79 .absolute()
80 .offset(None, Some(offset), Some(0.0), Some(0.0)))
81 .child(content),
82 ))
83}
84
85pub fn NavigationBar(selected_index: usize, items: Vec<NavItem>) -> View {
86 Row(Modifier::new()
87 .fill_max_size()
88 .background(theme().surface)
89 .padding(8.0))
90 .child(
91 items
92 .into_iter()
93 .enumerate()
94 .map(|(i, item)| NavigationBarItem(item, i == selected_index))
95 .collect::<Vec<_>>(),
96 )
97}
98
99pub struct NavItem {
100 pub icon: View,
101 pub label: String,
102 pub on_click: Rc<dyn Fn()>,
103}
104
105fn NavigationBarItem(item: NavItem, selected: bool) -> View {
106 let color = if selected {
107 theme().primary
108 } else {
109 theme().on_surface
110 };
111
112 Column(
113 Modifier::new()
114 .flex_grow(1.0)
115 .clickable()
116 .on_pointer_down(move |_| (item.on_click)()),
117 )
118 .child((
119 item.icon, Text(item.label).color(color),
121 ))
122}
123
124pub fn Card(modifier: Modifier, elevated: bool, content: View) -> View {
125 Surface(
126 modifier
127 .background(theme().surface)
128 .border(1.0, Color::from_hex("#22222222"), 12.0)
129 .clip_rounded(12.0)
130 .padding(16.0),
131 content,
132 )
133}
134
135pub fn Snackbar(
136 message: impl Into<String>,
137 action: Option<SnackbarAction>,
138 base_modifier: Modifier,
139) -> View {
140 let msg = message.into();
141 let th = theme();
142 let bg = th.surface_variant;
143 let fg = th.on_surface;
144 let action_color = th.primary;
145
146 let modifier = base_modifier
148 .background(bg)
149 .clip_rounded(th.shapes.small)
150 .border(1.0, th.outline_variant, th.shapes.small)
151 .padding_values(PaddingValues {
152 left: 16.0,
153 right: 16.0,
154 top: 12.0,
155 bottom: 12.0,
156 })
157 .min_height(48.0)
158 .min_width(280.0);
159
160 Surface(
161 modifier,
162 Row(Modifier::new().align_items(repose_core::AlignItems::Center)).child((
163 Text(msg)
164 .color(fg)
165 .size(th.typography.body_medium)
166 .max_lines(2)
167 .overflow_ellipsize(),
168 Spacer(),
169 action
170 .map(|a| {
171 let label = a.label.clone();
172 Box(Modifier::new()
173 .padding_values(PaddingValues {
174 left: 8.0,
175 right: 8.0,
176 top: 6.0,
177 bottom: 6.0,
178 })
179 .clip_rounded(th.shapes.extra_small)
180 .clickable()
181 .on_pointer_down(move |_| (a.on_click)()))
182 .child(
183 Text(label)
184 .color(action_color)
185 .size(th.typography.label_large)
186 .single_line(),
187 )
188 })
189 .unwrap_or(Box(Modifier::new())),
190 )),
191 )
192}
193
194pub fn OutlinedCard(modifier: Modifier, content: View) -> View {
195 Surface(
196 modifier
197 .border(1.0, Color::from_hex("#444444"), 12.0)
198 .clip_rounded(12.0)
199 .padding(16.0),
200 content,
201 )
202}
203
204pub fn FilterChip(
205 selected: bool,
206 on_click: impl Fn() + 'static,
207 label: View,
208 leading_icon: Option<View>,
209) -> View {
210 let bg = if selected {
211 theme().primary
212 } else {
213 theme().surface
214 };
215 let fg = if selected {
216 theme().on_primary
217 } else {
218 theme().on_surface
219 };
220
221 Surface(
222 Modifier::new()
223 .background(bg)
224 .border(1.0, Color::from_hex("#444444"), 8.0)
225 .clip_rounded(8.0)
226 .padding(12.0)
227 .clickable()
228 .on_pointer_down(move |_| on_click()),
229 Row(Modifier::new()).child((leading_icon.unwrap_or(Box(Modifier::new())), label)),
230 )
231}
232
233pub fn Scaffold(
234 top_bar: Option<View>,
235 bottom_bar: Option<View>,
236 floating_action_button: Option<View>,
237 content: impl Fn(PaddingValues) -> View,
238) -> View {
239 Stack(Modifier::new().fill_max_size()).child((
240 Box(Modifier::new()
242 .fill_max_size()
243 .padding_values(PaddingValues {
244 top: if top_bar.is_some() { 64.0 } else { 0.0 },
245 bottom: if bottom_bar.is_some() { 80.0 } else { 0.0 },
246 ..Default::default()
247 }))
248 .child(content(PaddingValues::default())),
249 if let Some(bar) = top_bar {
251 Box(Modifier::new()
252 .absolute()
253 .offset(Some(0.0), Some(0.0), Some(0.0), None))
254 .child(bar)
255 } else {
256 Box(Modifier::new())
257 },
258 if let Some(bar) = bottom_bar {
260 Box(Modifier::new()
261 .absolute()
262 .offset(Some(0.0), None, Some(0.0), Some(0.0)))
263 .child(bar)
264 } else {
265 Box(Modifier::new())
266 },
267 if let Some(fab) = floating_action_button {
269 Box(Modifier::new()
270 .absolute()
271 .offset(None, None, Some(16.0), Some(16.0)))
272 .child(fab)
273 } else {
274 Box(Modifier::new())
275 },
276 ))
277}