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