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 dragged: bool,
157 pub colors: ListItemColors,
158 pub state_colors: StateColors,
159 pub tonal_elevation: f32,
160 pub dragged_elevation: f32,
162 pub shadow_elevation: f32,
163 pub shape_radius: f32,
164 pub shape_radii: Option<[f32; 4]>,
166 pub horizontal_padding: f32,
167 pub trailing_padding: f32,
168 pub one_line_height: f32,
169 pub two_line_height: f32,
170 pub three_line_height: f32,
171 pub interaction_source: Option<MutableInteractionSource>,
172}
173
174impl Default for ListItemConfig {
175 fn default() -> Self {
176 Self {
177 modifier: Modifier::new(),
178 enabled: true,
179 selected: false,
180 dragged: false,
181 colors: ListItemColors::default(),
182 state_colors: ListItemDefaults::state_colors_default(),
183 tonal_elevation: 0.0,
184 dragged_elevation: 0.0,
185 shadow_elevation: 0.0,
186 shape_radius: 0.0,
187 shape_radii: None,
188 horizontal_padding: ListItemDefaults::HORIZONTAL_PADDING,
189 trailing_padding: ListItemDefaults::TRAILING_PADDING,
190 one_line_height: ListItemDefaults::ONE_LINE_HEIGHT,
191 two_line_height: ListItemDefaults::TWO_LINE_HEIGHT,
192 three_line_height: ListItemDefaults::THREE_LINE_HEIGHT,
193 interaction_source: None,
194 }
195 }
196}
197
198static LISTITEM_COUNTER: AtomicU64 = AtomicU64::new(0);
199
200pub fn ListItem(
203 headline: impl Into<String>,
204 supporting_text: Option<String>,
205 overline_text: Option<String>,
206 leading: Option<View>,
207 trailing: Option<View>,
208 on_click: Option<Rc<dyn Fn()>>,
209 on_long_click: Option<Rc<dyn Fn()>>,
210 config: ListItemConfig,
211) -> View {
212 let th = theme();
213 let is_enabled = config.enabled;
214 let is_selected = config.selected;
215 let is_dragged = config.dragged;
216 let c = &config.colors;
217 let id = remember(|| LISTITEM_COUNTER.fetch_add(1, Ordering::Relaxed));
218 let spec = th.motion.color;
219
220 let hd_col = animate_color(
221 format!("li_hd_{}", id),
222 c.headline(is_enabled, is_selected, is_dragged),
223 spec,
224 );
225 let sp_col = animate_color(
226 format!("li_sp_{}", id),
227 c.supporting(is_enabled, is_selected, is_dragged),
228 spec,
229 );
230 let ol_col = animate_color(
231 format!("li_ol_{}", id),
232 c.overline(is_enabled, is_selected, is_dragged),
233 spec,
234 );
235 let ld_col = animate_color(
236 format!("li_ld_{}", id),
237 c.leading_icon(is_enabled, is_selected, is_dragged),
238 spec,
239 );
240 let tr_col = animate_color(
241 format!("li_tr_{}", id),
242 c.trailing_icon(is_enabled, is_selected, is_dragged),
243 spec,
244 );
245 let bg = animate_color(
246 format!("li_bg_{}", id),
247 c.container(is_enabled, is_selected, is_dragged),
248 spec,
249 );
250
251 let line_count = match (overline_text.is_some(), supporting_text.is_some()) {
252 (true, true) => 3,
253 (true, false) | (false, true) => 2,
254 (false, false) => 1,
255 };
256 let min_h = match line_count {
257 3 => config.three_line_height,
258 2 => config.two_line_height,
259 _ => config.one_line_height,
260 };
261 let top_bottom_padding = match line_count {
262 3 => 12.0,
263 _ => 8.0,
264 };
265
266 let vert_align = if min_h >= config.three_line_height {
267 AlignItems::START
268 } else {
269 AlignItems::CENTER
270 };
271
272 let li_source: Rc<MutableInteractionSource> = config
273 .interaction_source
274 .clone()
275 .map(Rc::new)
276 .unwrap_or_else(|| remember(MutableInteractionSource::new));
277
278 let mut modifier = Modifier::new()
279 .min_width(200.0)
280 .min_height(min_h)
281 .background(bg);
282 match config.shape_radii {
283 Some(r) => modifier = modifier.clip_rounded_radii(r),
284 None => modifier = modifier.clip_rounded(config.shape_radius),
285 }
286 modifier = modifier
287 .state_colors(config.state_colors)
288 .padding_values(PaddingValues {
289 left: config.horizontal_padding,
290 right: config.trailing_padding,
291 top: top_bottom_padding,
292 bottom: top_bottom_padding,
293 })
294 .align_items(vert_align)
295 .interaction_source(&*li_source)
296 .then(config.modifier);
297
298 if config.tonal_elevation > 0.0 || config.dragged_elevation > 0.0 {
299 let dragged_elev = if config.dragged_elevation > 0.0 {
300 config.dragged_elevation
301 } else {
302 config.tonal_elevation
303 };
304 modifier = modifier.state_elevation(StateElevation {
305 default: config.tonal_elevation,
306 hovered: config.tonal_elevation,
307 pressed: config.tonal_elevation,
308 dragged: dragged_elev,
309 disabled: 0.0,
310 });
311 }
312 if config.shadow_elevation > 0.0 {
313 modifier = modifier.shadow(config.shadow_elevation, 0.0);
314 }
315
316 if on_click.is_some() || on_long_click.is_some() {
317 modifier = modifier.clickable();
318 if let Some(cb) = on_click {
319 let cb = cb.clone();
320 modifier = modifier.on_click(move || {
321 if is_enabled {
322 cb();
323 }
324 });
325 }
326 if let Some(cb) = &on_long_click {
327 let cb = cb.clone();
328 modifier = modifier.on_long_click(move || {
329 if is_enabled {
330 cb();
331 }
332 });
333 }
334 }
335
336 let wrap_icon = |color: Color, v: View| -> View { with_content_color(color, move || v) };
337
338 Row(modifier).child((
339 leading
340 .map(|v| {
341 Box(Modifier::new().padding_values(PaddingValues {
342 left: 0.0,
343 right: 16.0,
344 top: 0.0,
345 bottom: 0.0,
346 }))
347 .child(wrap_icon(ld_col, v))
348 })
349 .unwrap_or(Box(Modifier::new())),
350 Column(
351 Modifier::new()
352 .flex_grow(1.0)
353 .justify_content(JustifyContent::CENTER),
354 )
355 .child((
356 overline_text
357 .map(|ot| {
358 Text(ot)
359 .color(ol_col)
360 .size(th.typography.label_small)
361 .single_line()
362 })
363 .unwrap_or(Box(Modifier::new())),
364 Text(headline)
365 .color(hd_col)
366 .size(th.typography.body_large)
367 .single_line(),
368 supporting_text
369 .map(|st| {
370 Text(st)
371 .color(sp_col)
372 .size(th.typography.body_medium)
373 .max_lines(2)
374 .overflow_ellipsize()
375 })
376 .unwrap_or(Box(Modifier::new())),
377 )),
378 trailing
379 .map(|v| {
380 Box(Modifier::new().padding_values(PaddingValues {
381 left: 16.0,
382 right: 0.0,
383 top: 0.0,
384 bottom: 0.0,
385 }))
386 .child(wrap_icon(tr_col, v))
387 })
388 .unwrap_or(Box(Modifier::new())),
389 ))
390}
391
392pub fn SelectableListItem(
395 headline: impl Into<String>,
396 selected: bool,
397 supporting_text: Option<String>,
398 overline_text: Option<String>,
399 leading: Option<View>,
400 trailing: Option<View>,
401 on_click: Option<Rc<dyn Fn()>>,
402 mut config: ListItemConfig,
403) -> View {
404 config.selected = selected;
405 let mut m = Modifier::new().semantics(Semantics::new(Role::RadioButton));
406 m = m.then(config.modifier);
407 config.modifier = m;
408 ListItem(
409 headline,
410 supporting_text,
411 overline_text,
412 leading,
413 trailing,
414 on_click,
415 None,
416 config,
417 )
418}
419
420pub fn ToggleableListItem(
423 headline: impl Into<String>,
424 checked: bool,
425 on_checked_change: impl Fn(bool) + 'static,
426 supporting_text: Option<String>,
427 overline_text: Option<String>,
428 leading: Option<View>,
429 trailing: Option<View>,
430 config: ListItemConfig,
431) -> View {
432 let mut cfg = config.clone();
433 cfg.selected = checked;
434 let cb = Rc::new(on_checked_change);
435 let cb2 = cb.clone();
436 let mut m = Modifier::new().semantics(Semantics::new(Role::Checkbox));
437 m = m.then(cfg.modifier);
438 cfg.modifier = m;
439 ListItem(
440 headline,
441 supporting_text,
442 overline_text,
443 leading,
444 trailing,
445 Some(Rc::new(move || (cb2)(!checked))),
446 None,
447 cfg,
448 )
449}
450
451fn segmented_item_radii(index: usize, count: usize, r: f32) -> [f32; 4] {
453 if count <= 1 {
454 [r, r, r, r]
455 } else if index == 0 {
456 [0.0, 0.0, r, r]
457 } else if index == count - 1 {
458 [r, r, 0.0, 0.0]
459 } else {
460 [0.0, 0.0, 0.0, 0.0]
461 }
462}
463
464pub fn SegmentedListItem(
466 index: usize,
467 count: usize,
468 headline: impl Into<String>,
469 supporting_text: Option<String>,
470 overline_text: Option<String>,
471 leading: Option<View>,
472 trailing: Option<View>,
473 on_click: Option<Rc<dyn Fn()>>,
474 mut config: ListItemConfig,
475) -> View {
476 config.shape_radii = Some(segmented_item_radii(index, count, config.shape_radius));
477 ListItem(
478 headline,
479 supporting_text,
480 overline_text,
481 leading,
482 trailing,
483 on_click,
484 None,
485 config,
486 )
487}
488
489pub fn SegmentedSelectableListItem(
491 index: usize,
492 count: usize,
493 headline: impl Into<String>,
494 selected: bool,
495 supporting_text: Option<String>,
496 overline_text: Option<String>,
497 leading: Option<View>,
498 trailing: Option<View>,
499 on_click: Option<Rc<dyn Fn()>>,
500 mut config: ListItemConfig,
501) -> View {
502 config.selected = selected;
503 config.shape_radii = Some(segmented_item_radii(index, count, config.shape_radius));
504 let mut m = Modifier::new().semantics(Semantics::new(Role::RadioButton));
505 m = m.then(config.modifier);
506 config.modifier = m;
507 ListItem(
508 headline,
509 supporting_text,
510 overline_text,
511 leading,
512 trailing,
513 on_click,
514 None,
515 config,
516 )
517}
518
519pub fn SegmentedToggleableListItem(
521 index: usize,
522 count: usize,
523 headline: impl Into<String>,
524 checked: bool,
525 on_checked_change: impl Fn(bool) + 'static,
526 supporting_text: Option<String>,
527 overline_text: Option<String>,
528 leading: Option<View>,
529 trailing: Option<View>,
530 config: ListItemConfig,
531) -> View {
532 let mut cfg = config.clone();
533 cfg.selected = checked;
534 cfg.shape_radii = Some(segmented_item_radii(index, count, cfg.shape_radius));
535 let cb2 = Rc::new(on_checked_change);
536 let mut m = Modifier::new().semantics(Semantics::new(Role::Checkbox));
537 m = m.then(cfg.modifier);
538 cfg.modifier = m;
539 ListItem(
540 headline,
541 supporting_text,
542 overline_text,
543 leading,
544 trailing,
545 Some(Rc::new(move || (cb2)(!checked))),
546 None,
547 cfg,
548 )
549}