Skip to main content

frus_widgets/
carousel.rs

1//! [`CarouselView`]: ‹ › arrows around a **current slide** the application supplies, so
2//! it is controlled. Only one slide is realised at a time.
3
4use frus_core::{Rect, Scene};
5use frus_layout::{Align, FlexDirection, Style};
6
7use crate::button::{Button, Variant};
8use crate::flex::Flex;
9use crate::interaction::Status;
10use crate::theme::Theme;
11use crate::widget::Widget;
12
13/// A navigation arrow, disabled when `message` is `None`.
14fn arrow<Msg: Clone + 'static>(label: &str, message: Option<Msg>) -> Box<dyn Widget<Msg>> {
15    let mut button = Button::new(label).variant(Variant::Outlined).size(16.0);
16    if let Some(message) = message {
17        button = button.on_press(message);
18    }
19    Box::new(button)
20}
21
22/// A controlled carousel.
23pub struct CarouselView<Msg = crate::callback::Callback> {
24    children: Vec<Box<dyn Widget<Msg>>>,
25}
26
27impl<Msg: Clone + 'static> CarouselView<Msg> {
28    /// Creates the carousel: slide `index` of `count`, `on_change(i)` on the arrows,
29    /// and the current slide's content — the app knows what to show for `index`.
30    pub fn new(
31        index: usize,
32        count: usize,
33        on_change: impl Fn(usize) -> Msg + 'static,
34        slide: impl Widget<Msg> + 'static,
35    ) -> Self {
36        let prev = arrow("‹", (index > 0).then(|| on_change(index - 1)));
37        let next = arrow("›", (index + 1 < count).then(|| on_change(index + 1)));
38        Self {
39            children: vec![prev, Box::new(Flex::column().flex(1.0).child(slide)), next],
40        }
41    }
42}
43
44impl<Msg: Clone> Widget<Msg> for CarouselView<Msg> {
45    fn style(&self) -> Style {
46        Style {
47            flex_direction: FlexDirection::Row,
48            align: Align::Center,
49            gap: 12.0,
50            ..Default::default()
51        }
52    }
53
54    fn children(&self) -> &[Box<dyn Widget<Msg>>] {
55        &self.children
56    }
57
58    fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
59
60    fn on_click(&self) -> Option<Msg> {
61        None
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use crate::Text;
69
70    #[derive(Clone, Debug, PartialEq)]
71    enum Msg {
72        Go(usize),
73    }
74
75    #[test]
76    fn arrows_bounded_and_emit_change() {
77        // Slide 0 of 3: ‹ disabled, › goes to 1.
78        let first = CarouselView::new(0, 3, Msg::Go, Text::new("A"));
79        let c = Widget::<Msg>::children(&first);
80        assert_eq!(c.len(), 3);
81        assert_eq!(c[0].on_click(), None); // ‹ disabled at the start
82        assert_eq!(c[2].on_click(), Some(Msg::Go(1))); // › goes to slide 1
83
84        // Slide 2 of 3: ‹ goes to 1, › disabled.
85        let last = CarouselView::new(2, 3, Msg::Go, Text::new("C"));
86        let c = Widget::<Msg>::children(&last);
87        assert_eq!(c[0].on_click(), Some(Msg::Go(1)));
88        assert_eq!(c[2].on_click(), None);
89    }
90}