1#![allow(non_snake_case)]
2
3use std::rc::Rc;
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use repose_core::*;
7use repose_ui::{
8 Box, Column, Row, Text, TextStyle,
9 ViewExt,
10 anim::animate_color,
11};
12
13use super::*;
14
15#[derive(Clone, Debug)]
18pub struct ListItemColors {
19 pub container_color: Color,
20 pub headline_color: Color,
21 pub supporting_color: Color,
22 pub overline_color: Color,
23 pub leading_icon_color: Color,
24 pub trailing_icon_color: Color,
25
26 pub disabled_container_color: Color,
27 pub disabled_headline_color: Color,
28 pub disabled_supporting_color: Color,
29 pub disabled_overline_color: Color,
30 pub disabled_leading_icon_color: Color,
31 pub disabled_trailing_icon_color: Color,
32
33 pub selected_container_color: Color,
34 pub selected_headline_color: Color,
35 pub selected_supporting_color: Color,
36 pub selected_overline_color: Color,
37 pub selected_leading_icon_color: Color,
38 pub selected_trailing_icon_color: Color,
39
40 pub dragged_container_color: Color,
41 pub dragged_headline_color: Color,
42 pub dragged_supporting_color: Color,
43 pub dragged_overline_color: Color,
44 pub dragged_leading_icon_color: Color,
45 pub dragged_trailing_icon_color: Color,
46}
47
48impl ListItemColors {
49 pub fn container(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
50 if !enabled {
51 self.disabled_container_color
52 } else if dragged {
53 self.dragged_container_color
54 } else if selected {
55 self.selected_container_color
56 } else {
57 self.container_color
58 }
59 }
60 pub fn headline(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
61 if !enabled {
62 self.disabled_headline_color
63 } else if dragged {
64 self.dragged_headline_color
65 } else if selected {
66 self.selected_headline_color
67 } else {
68 self.headline_color
69 }
70 }
71 pub fn supporting(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
72 if !enabled {
73 self.disabled_supporting_color
74 } else if dragged {
75 self.dragged_supporting_color
76 } else if selected {
77 self.selected_supporting_color
78 } else {
79 self.supporting_color
80 }
81 }
82 pub fn overline(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
83 if !enabled {
84 self.disabled_overline_color
85 } else if dragged {
86 self.dragged_overline_color
87 } else if selected {
88 self.selected_overline_color
89 } else {
90 self.overline_color
91 }
92 }
93 pub fn leading_icon(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
94 if !enabled {
95 self.disabled_leading_icon_color
96 } else if dragged {
97 self.dragged_leading_icon_color
98 } else if selected {
99 self.selected_leading_icon_color
100 } else {
101 self.leading_icon_color
102 }
103 }
104 pub fn trailing_icon(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
105 if !enabled {
106 self.disabled_trailing_icon_color
107 } else if dragged {
108 self.dragged_trailing_icon_color
109 } else if selected {
110 self.selected_trailing_icon_color
111 } else {
112 self.trailing_icon_color
113 }
114 }
115}
116
117impl Default for ListItemColors {
118 fn default() -> Self {
119 Self {
120 container_color: Color::TRANSPARENT,
121 headline_color: ListItemDefaults::headline_color(),
122 supporting_color: ListItemDefaults::supporting_color(),
123 overline_color: ListItemDefaults::overline_color(),
124 leading_icon_color: ListItemDefaults::leading_icon_color(),
125 trailing_icon_color: ListItemDefaults::trailing_icon_color(),
126 disabled_container_color: ListItemDefaults::disabled_container_color(),
127 disabled_headline_color: ListItemDefaults::disabled_headline_color(),
128 disabled_supporting_color: ListItemDefaults::disabled_supporting_color(),
129 disabled_overline_color: ListItemDefaults::disabled_overline_color(),
130 disabled_leading_icon_color: ListItemDefaults::disabled_leading_icon_color(),
131 disabled_trailing_icon_color: ListItemDefaults::disabled_trailing_icon_color(),
132 selected_container_color: ListItemDefaults::selected_container_color(),
133 selected_headline_color: ListItemDefaults::selected_headline_color(),
134 selected_supporting_color: ListItemDefaults::selected_supporting_color(),
135 selected_overline_color: ListItemDefaults::selected_overline_color(),
136 selected_leading_icon_color: ListItemDefaults::selected_leading_icon_color(),
137 selected_trailing_icon_color: ListItemDefaults::selected_trailing_icon_color(),
138 dragged_container_color: ListItemDefaults::dragged_container_color(),
139 dragged_headline_color: ListItemDefaults::dragged_headline_color(),
140 dragged_supporting_color: ListItemDefaults::dragged_supporting_color(),
141 dragged_overline_color: ListItemDefaults::dragged_overline_color(),
142 dragged_leading_icon_color: ListItemDefaults::dragged_leading_icon_color(),
143 dragged_trailing_icon_color: ListItemDefaults::dragged_trailing_icon_color(),
144 }
145 }
146}
147
148#[derive(Clone, Debug)]
150pub struct ListItemConfig {
151 pub modifier: Modifier,
152 pub enabled: bool,
154 pub selected: bool,
155 pub colors: ListItemColors,
156 pub state_colors: StateColors,
157 pub tonal_elevation: f32,
158 pub shadow_elevation: f32,
159 pub shape_radius: f32,
160 pub shape_radii: Option<[f32; 4]>,
162 pub horizontal_padding: f32,
163 pub trailing_padding: f32,
164 pub one_line_height: f32,
165 pub two_line_height: f32,
166 pub three_line_height: f32,
167 pub interaction_source: Option<MutableInteractionSource>,
168}
169
170impl Default for ListItemConfig {
171 fn default() -> Self {
172 Self {
173 modifier: Modifier::new(),
174 enabled: true,
175 selected: false,
176 colors: ListItemColors::default(),
177 state_colors: ListItemDefaults::state_colors_default(),
178 tonal_elevation: 0.0,
179 shadow_elevation: 0.0,
180 shape_radius: 0.0,
181 shape_radii: None,
182 horizontal_padding: ListItemDefaults::HORIZONTAL_PADDING,
183 trailing_padding: ListItemDefaults::TRAILING_PADDING,
184 one_line_height: ListItemDefaults::ONE_LINE_HEIGHT,
185 two_line_height: ListItemDefaults::TWO_LINE_HEIGHT,
186 three_line_height: ListItemDefaults::THREE_LINE_HEIGHT,
187 interaction_source: None,
188 }
189 }
190}
191
192static LISTITEM_COUNTER: AtomicU64 = AtomicU64::new(0);
193
194pub fn ListItem(
197 headline: impl Into<String>,
198 supporting_text: Option<String>,
199 overline_text: Option<String>,
200 leading: Option<View>,
201 trailing: Option<View>,
202 on_click: Option<Rc<dyn Fn()>>,
203 on_long_click: Option<Rc<dyn Fn()>>,
204 config: ListItemConfig,
205) -> View {
206 let th = theme();
207 let is_enabled = config.enabled;
208 let is_selected = config.selected;
209 let c = &config.colors;
210 let id = remember(|| LISTITEM_COUNTER.fetch_add(1, Ordering::Relaxed));
211 let spec = th.motion.color;
212
213 let hd_col = animate_color(
214 format!("li_hd_{}", id),
215 c.headline(is_enabled, is_selected, false),
216 spec,
217 );
218 let sp_col = animate_color(
219 format!("li_sp_{}", id),
220 c.supporting(is_enabled, is_selected, false),
221 spec,
222 );
223 let ol_col = animate_color(
224 format!("li_ol_{}", id),
225 c.overline(is_enabled, is_selected, false),
226 spec,
227 );
228 let ld_col = animate_color(
229 format!("li_ld_{}", id),
230 c.leading_icon(is_enabled, is_selected, false),
231 spec,
232 );
233 let tr_col = animate_color(
234 format!("li_tr_{}", id),
235 c.trailing_icon(is_enabled, is_selected, false),
236 spec,
237 );
238 let bg = animate_color(
239 format!("li_bg_{}", id),
240 c.container(is_enabled, is_selected, false),
241 spec,
242 );
243
244 let line_count = match (overline_text.is_some(), supporting_text.is_some()) {
245 (true, true) => 3,
246 (true, false) | (false, true) => 2,
247 (false, false) => 1,
248 };
249 let min_h = match line_count {
250 3 => config.three_line_height,
251 2 => config.two_line_height,
252 _ => config.one_line_height,
253 };
254 let top_bottom_padding = match line_count {
255 3 => 12.0,
256 _ => 8.0,
257 };
258
259 let vert_align = if min_h >= config.three_line_height {
260 AlignItems::START
261 } else {
262 AlignItems::CENTER
263 };
264
265 let li_source: Rc<MutableInteractionSource> = config
266 .interaction_source
267 .clone()
268 .map(Rc::new)
269 .unwrap_or_else(|| remember(MutableInteractionSource::new));
270
271 let mut modifier = Modifier::new()
272 .min_width(200.0)
273 .min_height(min_h)
274 .background(bg);
275 match config.shape_radii {
276 Some(r) => modifier = modifier.clip_rounded_radii(r),
277 None => modifier = modifier.clip_rounded(config.shape_radius),
278 }
279 modifier = modifier
280 .state_colors(config.state_colors)
281 .padding_values(PaddingValues {
282 left: config.horizontal_padding,
283 right: config.trailing_padding,
284 top: top_bottom_padding,
285 bottom: top_bottom_padding,
286 })
287 .align_items(vert_align)
288 .interaction_source(&*li_source)
289 .then(config.modifier);
290
291 if config.tonal_elevation > 0.0 {
292 modifier = modifier.state_elevation(StateElevation {
293 default: config.tonal_elevation,
294 hovered: config.tonal_elevation,
295 pressed: config.tonal_elevation,
296 disabled: 0.0,
297 });
298 }
299 if config.shadow_elevation > 0.0 {
300 modifier = modifier.shadow(config.shadow_elevation, 0.0);
301 }
302
303 if on_click.is_some() || on_long_click.is_some() {
304 modifier = modifier.clickable();
305 if let Some(cb) = on_click {
306 let cb = cb.clone();
307 modifier = modifier.on_click(move || {
308 if is_enabled {
309 cb();
310 }
311 });
312 }
313 if let Some(cb) = &on_long_click {
314 let cb = cb.clone();
315 modifier = modifier.on_long_click(move || {
316 if is_enabled {
317 cb();
318 }
319 });
320 }
321 }
322
323 let wrap_icon = |color: Color, v: View| -> View { with_content_color(color, move || v) };
324
325 Row(modifier).child((
326 leading
327 .map(|v| {
328 Box(Modifier::new().padding_values(PaddingValues {
329 left: 0.0,
330 right: 16.0,
331 top: 0.0,
332 bottom: 0.0,
333 }))
334 .child(wrap_icon(ld_col, v))
335 })
336 .unwrap_or(Box(Modifier::new())),
337 Column(
338 Modifier::new()
339 .flex_grow(1.0)
340 .justify_content(JustifyContent::CENTER),
341 )
342 .child((
343 overline_text
344 .map(|ot| {
345 Text(ot)
346 .color(ol_col)
347 .size(th.typography.label_small)
348 .single_line()
349 })
350 .unwrap_or(Box(Modifier::new())),
351 Text(headline)
352 .color(hd_col)
353 .size(th.typography.body_large)
354 .single_line(),
355 supporting_text
356 .map(|st| {
357 Text(st)
358 .color(sp_col)
359 .size(th.typography.body_medium)
360 .max_lines(2)
361 .overflow_ellipsize()
362 })
363 .unwrap_or(Box(Modifier::new())),
364 )),
365 trailing
366 .map(|v| {
367 Box(Modifier::new().padding_values(PaddingValues {
368 left: 16.0,
369 right: 0.0,
370 top: 0.0,
371 bottom: 0.0,
372 }))
373 .child(wrap_icon(tr_col, v))
374 })
375 .unwrap_or(Box(Modifier::new())),
376 ))
377}
378
379pub fn SelectableListItem(
382 headline: impl Into<String>,
383 selected: bool,
384 supporting_text: Option<String>,
385 overline_text: Option<String>,
386 leading: Option<View>,
387 trailing: Option<View>,
388 on_click: Option<Rc<dyn Fn()>>,
389 mut config: ListItemConfig,
390) -> View {
391 config.selected = selected;
392 let mut m = Modifier::new().semantics(Semantics::new(Role::RadioButton));
393 m = m.then(config.modifier);
394 config.modifier = m;
395 ListItem(
396 headline,
397 supporting_text,
398 overline_text,
399 leading,
400 trailing,
401 on_click,
402 None,
403 config,
404 )
405}
406
407pub fn ToggleableListItem(
410 headline: impl Into<String>,
411 checked: bool,
412 on_checked_change: impl Fn(bool) + 'static,
413 supporting_text: Option<String>,
414 overline_text: Option<String>,
415 leading: Option<View>,
416 trailing: Option<View>,
417 config: ListItemConfig,
418) -> View {
419 let mut cfg = config.clone();
420 cfg.selected = checked;
421 let cb = Rc::new(on_checked_change);
422 let cb2 = cb.clone();
423 let mut m = Modifier::new().semantics(Semantics::new(Role::Checkbox));
424 m = m.then(cfg.modifier);
425 cfg.modifier = m;
426 ListItem(
427 headline,
428 supporting_text,
429 overline_text,
430 leading,
431 trailing,
432 Some(Rc::new(move || (cb2)(!checked))),
433 None,
434 cfg,
435 )
436}
437
438fn segmented_item_radii(index: usize, count: usize, r: f32) -> [f32; 4] {
440 if count <= 1 {
441 [r, r, r, r]
442 } else if index == 0 {
443 [0.0, 0.0, r, r]
444 } else if index == count - 1 {
445 [r, r, 0.0, 0.0]
446 } else {
447 [0.0, 0.0, 0.0, 0.0]
448 }
449}
450
451pub fn SegmentedListItem(
453 index: usize,
454 count: usize,
455 headline: impl Into<String>,
456 supporting_text: Option<String>,
457 overline_text: Option<String>,
458 leading: Option<View>,
459 trailing: Option<View>,
460 on_click: Option<Rc<dyn Fn()>>,
461 mut config: ListItemConfig,
462) -> View {
463 config.shape_radii = Some(segmented_item_radii(index, count, config.shape_radius));
464 ListItem(
465 headline,
466 supporting_text,
467 overline_text,
468 leading,
469 trailing,
470 on_click,
471 None,
472 config,
473 )
474}
475
476pub fn SegmentedSelectableListItem(
478 index: usize,
479 count: usize,
480 headline: impl Into<String>,
481 selected: bool,
482 supporting_text: Option<String>,
483 overline_text: Option<String>,
484 leading: Option<View>,
485 trailing: Option<View>,
486 on_click: Option<Rc<dyn Fn()>>,
487 mut config: ListItemConfig,
488) -> View {
489 config.selected = selected;
490 config.shape_radii = Some(segmented_item_radii(index, count, config.shape_radius));
491 let mut m = Modifier::new().semantics(Semantics::new(Role::RadioButton));
492 m = m.then(config.modifier);
493 config.modifier = m;
494 ListItem(
495 headline,
496 supporting_text,
497 overline_text,
498 leading,
499 trailing,
500 on_click,
501 None,
502 config,
503 )
504}
505
506pub fn SegmentedToggleableListItem(
508 index: usize,
509 count: usize,
510 headline: impl Into<String>,
511 checked: bool,
512 on_checked_change: impl Fn(bool) + 'static,
513 supporting_text: Option<String>,
514 overline_text: Option<String>,
515 leading: Option<View>,
516 trailing: Option<View>,
517 config: ListItemConfig,
518) -> View {
519 let mut cfg = config.clone();
520 cfg.selected = checked;
521 cfg.shape_radii = Some(segmented_item_radii(index, count, cfg.shape_radius));
522 let cb2 = Rc::new(on_checked_change);
523 let mut m = Modifier::new().semantics(Semantics::new(Role::Checkbox));
524 m = m.then(cfg.modifier);
525 cfg.modifier = m;
526 ListItem(
527 headline,
528 supporting_text,
529 overline_text,
530 leading,
531 trailing,
532 Some(Rc::new(move || (cb2)(!checked))),
533 None,
534 cfg,
535 )
536}