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