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