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 .then(config.modifier);
293
294 if config.tonal_elevation > 0.0 || config.dragged_elevation > 0.0 {
295 let dragged_elev = if config.dragged_elevation > 0.0 {
296 config.dragged_elevation
297 } else {
298 config.tonal_elevation
299 };
300 modifier = modifier.state_elevation(StateElevation {
301 default: config.tonal_elevation,
302 hovered: config.tonal_elevation,
303 pressed: config.tonal_elevation,
304 dragged: dragged_elev,
305 disabled: 0.0,
306 });
307 }
308 if config.shadow_elevation > 0.0 {
309 modifier = modifier.shadow(config.shadow_elevation, 0.0);
310 }
311
312 if on_click.is_some() || on_long_click.is_some() {
313 modifier = modifier.clickable();
314 if let Some(cb) = on_click {
315 let cb = cb.clone();
316 modifier = modifier.on_click(move || {
317 if is_enabled {
318 cb();
319 }
320 });
321 }
322 if let Some(cb) = &on_long_click {
323 let cb = cb.clone();
324 modifier = modifier.on_long_click(move || {
325 if is_enabled {
326 cb();
327 }
328 });
329 }
330 }
331
332 let wrap_icon = |color: Color, v: View| -> View { with_content_color(color, move || v) };
333
334 Row(modifier).child((
335 leading
336 .map(|v| {
337 Box(Modifier::new().padding_values(PaddingValues {
338 left: 0.0,
339 right: 16.0,
340 top: 0.0,
341 bottom: 0.0,
342 }))
343 .child(wrap_icon(ld_col, v))
344 })
345 .unwrap_or(Box(Modifier::new())),
346 Column(
347 Modifier::new()
348 .flex_grow(1.0)
349 .justify_content(JustifyContent::CENTER),
350 )
351 .child((
352 overline_text
353 .map(|ot| {
354 Text(ot)
355 .color(ol_col)
356 .size(th.typography.label_small)
357 .single_line()
358 })
359 .unwrap_or(Box(Modifier::new())),
360 Text(headline)
361 .color(hd_col)
362 .size(th.typography.body_large)
363 .single_line(),
364 supporting_text
365 .map(|st| {
366 Text(st)
367 .color(sp_col)
368 .size(th.typography.body_medium)
369 .max_lines(2)
370 .overflow_ellipsize()
371 })
372 .unwrap_or(Box(Modifier::new())),
373 )),
374 trailing
375 .map(|v| {
376 Box(Modifier::new().padding_values(PaddingValues {
377 left: 16.0,
378 right: 0.0,
379 top: 0.0,
380 bottom: 0.0,
381 }))
382 .child(wrap_icon(tr_col, v))
383 })
384 .unwrap_or(Box(Modifier::new())),
385 ))
386}
387
388pub fn SelectableListItem(
391 headline: impl Into<String>,
392 selected: bool,
393 supporting_text: Option<String>,
394 overline_text: Option<String>,
395 leading: Option<View>,
396 trailing: Option<View>,
397 on_click: Option<Rc<dyn Fn()>>,
398 mut config: ListItemConfig,
399) -> View {
400 config.selected = selected;
401 let mut m = Modifier::new().semantics(Semantics::new(Role::RadioButton));
402 m = m.then(config.modifier);
403 config.modifier = m;
404 ListItem(
405 headline,
406 supporting_text,
407 overline_text,
408 leading,
409 trailing,
410 on_click,
411 None,
412 config,
413 )
414}
415
416pub fn ToggleableListItem(
419 headline: impl Into<String>,
420 checked: bool,
421 on_checked_change: impl Fn(bool) + 'static,
422 supporting_text: Option<String>,
423 overline_text: Option<String>,
424 leading: Option<View>,
425 trailing: Option<View>,
426 config: ListItemConfig,
427) -> View {
428 let mut cfg = config.clone();
429 cfg.selected = checked;
430 let cb = Rc::new(on_checked_change);
431 let cb2 = cb.clone();
432 let mut m = Modifier::new().semantics(Semantics::new(Role::Checkbox));
433 m = m.then(cfg.modifier);
434 cfg.modifier = m;
435 ListItem(
436 headline,
437 supporting_text,
438 overline_text,
439 leading,
440 trailing,
441 Some(Rc::new(move || (cb2)(!checked))),
442 None,
443 cfg,
444 )
445}
446
447fn segmented_item_radii(index: usize, count: usize, r: f32) -> [f32; 4] {
449 if count <= 1 {
450 [r, r, r, r]
451 } else if index == 0 {
452 [0.0, 0.0, r, r]
453 } else if index == count - 1 {
454 [r, r, 0.0, 0.0]
455 } else {
456 [0.0, 0.0, 0.0, 0.0]
457 }
458}
459
460pub fn SegmentedListItem(
462 index: usize,
463 count: usize,
464 headline: impl Into<String>,
465 supporting_text: Option<String>,
466 overline_text: Option<String>,
467 leading: Option<View>,
468 trailing: Option<View>,
469 on_click: Option<Rc<dyn Fn()>>,
470 mut config: ListItemConfig,
471) -> View {
472 config.shape_radii = Some(segmented_item_radii(index, count, config.shape_radius));
473 ListItem(
474 headline,
475 supporting_text,
476 overline_text,
477 leading,
478 trailing,
479 on_click,
480 None,
481 config,
482 )
483}
484
485pub fn SegmentedSelectableListItem(
487 index: usize,
488 count: usize,
489 headline: impl Into<String>,
490 selected: bool,
491 supporting_text: Option<String>,
492 overline_text: Option<String>,
493 leading: Option<View>,
494 trailing: Option<View>,
495 on_click: Option<Rc<dyn Fn()>>,
496 mut config: ListItemConfig,
497) -> View {
498 config.selected = selected;
499 config.shape_radii = Some(segmented_item_radii(index, count, config.shape_radius));
500 let mut m = Modifier::new().semantics(Semantics::new(Role::RadioButton));
501 m = m.then(config.modifier);
502 config.modifier = m;
503 ListItem(
504 headline,
505 supporting_text,
506 overline_text,
507 leading,
508 trailing,
509 on_click,
510 None,
511 config,
512 )
513}
514
515pub fn SegmentedToggleableListItem(
517 index: usize,
518 count: usize,
519 headline: impl Into<String>,
520 checked: bool,
521 on_checked_change: impl Fn(bool) + 'static,
522 supporting_text: Option<String>,
523 overline_text: Option<String>,
524 leading: Option<View>,
525 trailing: Option<View>,
526 config: ListItemConfig,
527) -> View {
528 let mut cfg = config.clone();
529 cfg.selected = checked;
530 cfg.shape_radii = Some(segmented_item_radii(index, count, cfg.shape_radius));
531 let cb2 = Rc::new(on_checked_change);
532 let mut m = Modifier::new().semantics(Semantics::new(Role::Checkbox));
533 m = m.then(cfg.modifier);
534 cfg.modifier = m;
535 ListItem(
536 headline,
537 supporting_text,
538 overline_text,
539 leading,
540 trailing,
541 Some(Rc::new(move || (cb2)(!checked))),
542 None,
543 cfg,
544 )
545}