1use std::cell::{OnceCell, RefCell};
12
13use frus_core::{Color, Insets, Rect, Scene, ShapeBorder, TextStyle};
14use frus_layout::{FlexDirection, Style};
15
16use crate::collapsible::ControlAffinity;
17use crate::interaction::Status;
18use crate::theme::Theme;
19use crate::widget::Widget;
20use crate::{Checkbox, ListTile, Radio, Switch};
21
22struct Row<Msg> {
25 title: Option<String>,
26 subtitle: Option<String>,
27 secondary: RefCell<Option<Box<dyn Widget<Msg>>>>,
30 affinity: ControlAffinity,
31 dense: bool,
32 three_line: bool,
33 selected: bool,
34 enabled: bool,
35 tile_color: Option<Color>,
36 selected_tile_color: Option<Color>,
37 shape: Option<ShapeBorder>,
38 padding: Option<Insets>,
39 title_style: Option<TextStyle>,
40 subtitle_style: Option<TextStyle>,
41}
42
43impl<Msg> Default for Row<Msg> {
44 fn default() -> Self {
45 Self {
46 title: None,
47 subtitle: None,
48 secondary: RefCell::new(None),
49 affinity: ControlAffinity::Trailing,
50 dense: false,
51 three_line: false,
52 selected: false,
53 enabled: true,
54 tile_color: None,
55 selected_tile_color: None,
56 shape: None,
57 padding: None,
58 title_style: None,
59 subtitle_style: None,
60 }
61 }
62}
63
64impl<Msg: Clone + 'static> Row<Msg> {
65 fn tile(&self, control: Box<dyn Widget<Msg>>, on_tap: Option<Msg>) -> ListTile<Msg> {
72 let mut tile = ListTile::new();
73 if let Some(title) = &self.title {
74 tile = tile.title(title.clone());
75 }
76 if let Some(subtitle) = &self.subtitle {
77 tile = tile.subtitle(subtitle.clone());
78 }
79 if self.three_line {
80 tile = tile.three_line();
81 }
82 if self.dense {
83 tile = tile.dense();
84 }
85 if let Some(color) = self.tile_color {
86 tile = tile.tile_color(color);
87 }
88 if let Some(color) = self.selected_tile_color {
89 tile = tile.selected_tile_color(color);
90 }
91 if let Some(shape) = self.shape {
92 tile = tile.shape(shape);
93 }
94 if let Some(padding) = self.padding {
95 tile = tile.padding(padding);
96 }
97 if let Some(style) = self.title_style {
98 tile = tile.title_style(style);
99 }
100 if let Some(style) = self.subtitle_style {
101 tile = tile.subtitle_style(style);
102 }
103 tile = tile.selected(self.selected).enabled(self.enabled);
104 if let Some(message) = on_tap {
105 tile = tile.on_tap(message);
106 }
107
108 let other = self.secondary.borrow_mut().take();
109 let (leading, trailing) = match self.affinity {
110 ControlAffinity::Leading => (Some(control), other),
111 ControlAffinity::Trailing => (other, Some(control)),
112 };
113 if let Some(widget) = leading {
114 tile = tile.leading(crate::ConstrainedBox::new_boxed(widget));
115 }
116 if let Some(widget) = trailing {
117 tile = tile.trailing(crate::ConstrainedBox::new_boxed(widget));
118 }
119 tile
120 }
121}
122
123macro_rules! row_builders {
125 () => {
126 #[must_use]
128 pub fn title(mut self, title: impl Into<String>) -> Self {
129 self.row.title = Some(title.into());
130 self
131 }
132
133 #[must_use]
135 pub fn subtitle(mut self, subtitle: impl Into<String>) -> Self {
136 self.row.subtitle = Some(subtitle.into());
137 self
138 }
139
140 #[must_use]
144 pub fn secondary(self, widget: impl Widget<Msg> + 'static) -> Self {
145 *self.row.secondary.borrow_mut() = Some(Box::new(widget));
146 self
147 }
148
149 #[must_use]
153 pub fn control_affinity(mut self, affinity: ControlAffinity) -> Self {
154 self.row.affinity = affinity;
155 self
156 }
157
158 #[must_use]
160 pub fn dense(mut self) -> Self {
161 self.row.dense = true;
162 self
163 }
164
165 #[must_use]
167 pub fn three_line(mut self) -> Self {
168 self.row.three_line = true;
169 self
170 }
171
172 #[must_use]
175 pub fn selected(mut self, selected: bool) -> Self {
176 self.row.selected = selected;
177 self
178 }
179
180 #[must_use]
184 pub fn enabled(mut self, enabled: bool) -> Self {
185 self.row.enabled = enabled;
186 self
187 }
188
189 #[must_use]
191 pub fn tile_color(mut self, color: Color) -> Self {
192 self.row.tile_color = Some(color);
193 self
194 }
195
196 #[must_use]
198 pub fn selected_tile_color(mut self, color: Color) -> Self {
199 self.row.selected_tile_color = Some(color);
200 self
201 }
202
203 #[must_use]
205 pub fn shape(mut self, shape: ShapeBorder) -> Self {
206 self.row.shape = Some(shape);
207 self
208 }
209
210 #[must_use]
212 pub fn content_padding(mut self, padding: Insets) -> Self {
213 self.row.padding = Some(padding);
214 self
215 }
216
217 #[must_use]
219 pub fn title_style(mut self, style: TextStyle) -> Self {
220 self.row.title_style = Some(style);
221 self
222 }
223
224 #[must_use]
226 pub fn subtitle_style(mut self, style: TextStyle) -> Self {
227 self.row.subtitle_style = Some(style);
228 self
229 }
230 };
231}
232
233pub struct CheckboxListTile<Msg = crate::callback::Callback> {
245 row: Row<Msg>,
246 value: Option<bool>,
247 on_changed: Msg,
248 active_color: Option<Color>,
249 check_color: Option<Color>,
250 checkbox_radius: Option<f32>,
251 built: OnceCell<Vec<Box<dyn Widget<Msg>>>>,
252}
253
254impl<Msg: Clone + 'static> CheckboxListTile<Msg> {
255 pub fn new(value: bool, on_changed: Msg) -> Self {
258 Self {
259 row: Row::default(),
260 value: Some(value),
261 on_changed,
262 active_color: None,
263 check_color: None,
264 checkbox_radius: None,
265 built: OnceCell::new(),
266 }
267 }
268
269 pub fn maybe(value: Option<bool>, on_changed: Msg) -> Self {
272 Self {
273 value,
274 ..Self::new(false, on_changed)
275 }
276 }
277
278 row_builders!();
279
280 #[must_use]
282 pub fn active_color(mut self, color: Color) -> Self {
283 self.active_color = Some(color);
284 self
285 }
286
287 #[must_use]
289 pub fn check_color(mut self, color: Color) -> Self {
290 self.check_color = Some(color);
291 self
292 }
293
294 #[must_use]
296 pub fn checkbox_radius(mut self, radius: f32) -> Self {
297 self.checkbox_radius = Some(radius);
298 self
299 }
300
301 fn assemble(&self) -> Box<dyn Widget<Msg>> {
302 let mut checkbox = Checkbox::<Msg>::maybe(self.value).enabled(self.row.enabled);
303 if let Some(color) = self.active_color {
304 checkbox = checkbox.fill_color(color);
305 }
306 if let Some(color) = self.check_color {
307 checkbox = checkbox.check_color(color);
308 }
309 if let Some(radius) = self.checkbox_radius {
310 checkbox = checkbox.radius(radius);
311 }
312 if self.row.enabled {
316 let message = self.on_changed.clone();
317 checkbox = checkbox.on_change(move |_| message.clone());
318 }
319 let on_tap = self.row.enabled.then(|| self.on_changed.clone());
320 Box::new(self.row.tile(Box::new(checkbox), on_tap))
321 }
322}
323
324pub struct RadioListTile<Msg = crate::callback::Callback> {
332 row: Row<Msg>,
333 selected: bool,
334 on_changed: Msg,
335 active_color: Option<Color>,
336 built: OnceCell<Vec<Box<dyn Widget<Msg>>>>,
337}
338
339impl<Msg: Clone + 'static> RadioListTile<Msg> {
340 pub fn new(selected: bool, on_changed: Msg) -> Self {
343 Self {
344 row: Row::default(),
345 selected,
346 on_changed,
347 active_color: None,
348 built: OnceCell::new(),
349 }
350 }
351
352 row_builders!();
353
354 #[must_use]
356 pub fn active_color(mut self, color: Color) -> Self {
357 self.active_color = Some(color);
358 self
359 }
360
361 fn assemble(&self) -> Box<dyn Widget<Msg>> {
362 let mut radio = Radio::<Msg>::new(self.selected).enabled(self.row.enabled);
363 if let Some(color) = self.active_color {
364 radio = radio.selected_color(color);
365 }
366 if self.row.enabled {
367 radio = radio.on_select(self.on_changed.clone());
368 }
369 let on_tap = self.row.enabled.then(|| self.on_changed.clone());
370 Box::new(self.row.tile(Box::new(radio), on_tap))
371 }
372}
373
374pub struct SwitchListTile<Msg = crate::callback::Callback> {
378 row: Row<Msg>,
379 value: bool,
380 on_changed: Msg,
381 active_color: Option<Color>,
382 track_color: Option<Color>,
383 built: OnceCell<Vec<Box<dyn Widget<Msg>>>>,
384}
385
386impl<Msg: Clone + 'static> SwitchListTile<Msg> {
387 pub fn new(value: bool, on_changed: Msg) -> Self {
390 Self {
391 row: Row::default(),
392 value,
393 on_changed,
394 active_color: None,
395 track_color: None,
396 built: OnceCell::new(),
397 }
398 }
399
400 row_builders!();
401
402 #[must_use]
404 pub fn active_color(mut self, color: Color) -> Self {
405 self.active_color = Some(color);
406 self
407 }
408
409 #[must_use]
411 pub fn active_track_color(mut self, color: Color) -> Self {
412 self.track_color = Some(color);
413 self
414 }
415
416 fn assemble(&self) -> Box<dyn Widget<Msg>> {
417 let mut switch = Switch::<Msg>::new(self.value).enabled(self.row.enabled);
418 if let Some(color) = self.active_color {
419 switch = switch.thumb_color(color);
420 }
421 if let Some(color) = self.track_color {
422 switch = switch.track_color(color);
423 }
424 if self.row.enabled {
425 let message = self.on_changed.clone();
426 switch = switch.on_toggle(move |_| message.clone());
427 }
428 let on_tap = self.row.enabled.then(|| self.on_changed.clone());
429 Box::new(self.row.tile(Box::new(switch), on_tap))
430 }
431}
432
433impl<Msg: Clone + 'static> Widget<Msg> for CheckboxListTile<Msg> {
445 fn style(&self) -> Style {
448 Style {
449 flex_direction: FlexDirection::Column,
450 ..Default::default()
451 }
452 }
453
454 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
455 self.built.get().map(Vec::as_slice).unwrap_or(&[])
456 }
457
458 fn build_themed(&self, _theme: &Theme) {
461 let _ = self.built.set(vec![self.assemble()]);
462 }
463
464 fn paint(&self, _b: Rect, _s: Status, _t: &Theme, _scene: &mut Scene) {}
465
466 fn on_click(&self) -> Option<Msg> {
469 None
470 }
471}
472
473impl<Msg: Clone + 'static> Widget<Msg> for RadioListTile<Msg> {
474 fn style(&self) -> Style {
475 Style {
476 flex_direction: FlexDirection::Column,
477 ..Default::default()
478 }
479 }
480
481 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
482 self.built.get().map(Vec::as_slice).unwrap_or(&[])
483 }
484
485 fn build_themed(&self, _theme: &Theme) {
486 let _ = self.built.set(vec![self.assemble()]);
487 }
488
489 fn paint(&self, _b: Rect, _s: Status, _t: &Theme, _scene: &mut Scene) {}
490
491 fn on_click(&self) -> Option<Msg> {
493 None
494 }
495}
496
497impl<Msg: Clone + 'static> Widget<Msg> for SwitchListTile<Msg> {
498 fn style(&self) -> Style {
499 Style {
500 flex_direction: FlexDirection::Column,
501 ..Default::default()
502 }
503 }
504
505 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
506 self.built.get().map(Vec::as_slice).unwrap_or(&[])
507 }
508
509 fn build_themed(&self, _theme: &Theme) {
510 let _ = self.built.set(vec![self.assemble()]);
511 }
512
513 fn paint(&self, _b: Rect, _s: Status, _t: &Theme, _scene: &mut Scene) {}
514
515 fn on_click(&self) -> Option<Msg> {
517 None
518 }
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524 use crate::{build_ui, Icon, Icons, Point as P, Runtime, Size};
525
526 #[derive(Clone, Debug, PartialEq)]
527 enum Msg {
528 Toggle,
529 Other,
530 }
531
532 fn frame(widget: &dyn Widget<Msg>) -> crate::Ui<Msg> {
533 build_ui(
534 widget,
535 Size::new(320.0, 120.0),
536 &Runtime::default(),
537 &Theme::default(),
538 )
539 }
540
541 #[test]
546 fn a_tap_anywhere_on_the_row_works_the_control() {
547 for tile in [
548 Box::new(CheckboxListTile::new(false, Msg::Toggle).title("Notify me"))
549 as Box<dyn Widget<Msg>>,
550 Box::new(RadioListTile::new(false, Msg::Toggle).title("Notify me")),
551 Box::new(SwitchListTile::new(false, Msg::Toggle).title("Notify me")),
552 ] {
553 let ui = frame(tile.as_ref());
554 let hit = ui.hit(P::new(80.0, 24.0)).expect("the row is hittable");
556 assert_eq!(
557 ui.msg_for(hit),
558 Some(Msg::Toggle),
559 "the label answers, not only the box"
560 );
561 }
562 }
563
564 #[test]
568 fn the_control_and_the_row_say_the_same_thing() {
569 let tile = CheckboxListTile::new(false, Msg::Toggle).title("Notify me");
570 let ui = frame(&tile);
571 let box_hit = ui.hit(P::new(300.0, 24.0)).expect("the box is hittable");
573 assert_eq!(ui.msg_for(box_hit), Some(Msg::Toggle));
574 }
575
576 #[test]
580 fn a_disabled_row_answers_nowhere() {
581 let tile = SwitchListTile::new(true, Msg::Toggle)
582 .title("Notify me")
583 .enabled(false);
584 let ui = frame(&tile);
585 for x in [80.0, 300.0] {
586 if let Some(hit) = ui.hit(P::new(x, 24.0)) {
587 assert_ne!(ui.msg_for(hit), Some(Msg::Toggle), "at x = {x}");
588 }
589 }
590 }
591
592 #[test]
597 fn the_control_takes_the_side_it_was_given_and_the_icon_the_other() {
598 let ends = |tile: &dyn Widget<Msg>| {
599 let ui = frame(tile);
600 let mut xs: Vec<f32> = Vec::new();
603 fn walk(primitives: &[frus_core::Primitive], xs: &mut Vec<f32>) {
604 for p in primitives {
605 match p {
606 frus_core::Primitive::Rect { rect, .. } if rect.width < 100.0 => {
607 xs.push(rect.x)
608 }
609 frus_core::Primitive::Path { .. } => {}
610 frus_core::Primitive::Layer { primitives, .. } => walk(primitives, xs),
611 _ => {}
612 }
613 }
614 }
615 walk(ui.scene().primitives(), &mut xs);
616 xs
617 };
618
619 let trailing = CheckboxListTile::new(true, Msg::Toggle)
620 .title("Notify me")
621 .secondary(Icon::new(Icons::ADD));
622 let leading = CheckboxListTile::new(true, Msg::Toggle)
623 .title("Notify me")
624 .secondary(Icon::new(Icons::ADD))
625 .control_affinity(ControlAffinity::Leading);
626
627 let (right, left) = (ends(&trailing), ends(&leading));
628 assert!(!right.is_empty() && !left.is_empty(), "both drew a box");
629 let far_right = right.iter().cloned().fold(f32::MIN, f32::max);
630 let far_left = left.iter().cloned().fold(f32::MIN, f32::max);
631 assert!(
632 far_right > far_left,
633 "a trailing box sits further along the row than a leading one: \
634 {far_right} vs {far_left}"
635 );
636 }
637
638 #[test]
643 fn the_builder_order_does_not_change_what_comes_out() {
644 let first = SwitchListTile::new(true, Msg::Toggle)
645 .dense()
646 .title("Notify me")
647 .tile_color(Color::rgb(0.2, 0.3, 0.4));
648 let last = SwitchListTile::new(true, Msg::Toggle)
649 .tile_color(Color::rgb(0.2, 0.3, 0.4))
650 .title("Notify me")
651 .dense();
652 let paint = |tile: &SwitchListTile<Msg>| {
653 let ui = frame(tile);
654 format!("{:?}", ui.scene().primitives())
655 };
656 assert_eq!(paint(&first), paint(&last));
657 }
658
659 #[test]
662 fn being_the_chosen_row_is_not_being_on() {
663 let tile = RadioListTile::new(false, Msg::Toggle)
664 .title("Every day")
665 .selected(true)
666 .selected_tile_color(Color::rgb(0.6, 0.4, 0.2));
667 let ui = frame(&tile);
668 fn find(primitives: &[frus_core::Primitive], want: Color) -> bool {
669 primitives.iter().any(|p| match p {
670 frus_core::Primitive::Rect { color, .. } => *color == want,
671 frus_core::Primitive::Layer { primitives, .. } => find(primitives, want),
672 _ => false,
673 })
674 }
675 assert!(
676 find(ui.scene().primitives(), Color::rgb(0.6, 0.4, 0.2)),
677 "the selected surface is painted"
678 );
679 assert_ne!(Msg::Other, Msg::Toggle);
680 }
681}