Skip to main content

vizia_core/views/
collapsible.rs

1use crate::{icons::ICON_CHEVRON_DOWN, prelude::*};
2
3/// Events that can be triggered by the collapsible view.
4pub enum CollapsibleEvent {
5    ToggleOpen,
6}
7
8/// A collapsible view that can be opened or closed to hide content.
9///
10/// # Example
11/// ```no_run
12/// # use vizia_core::prelude::*;
13/// # let cx = &mut Context::default();
14/// Collapsible::new(
15///     cx,
16///     |cx| {
17///         Label::new(cx, "Click me to collapse the content").hoverable(false);
18///     },
19///     |cx| {
20///         Label::new(cx, "Line 1\nLine 2\nLine 3\nLine 4\nLine 5").hoverable(false);
21///     },
22/// )
23/// .width(Pixels(300.0));
24/// ```
25pub struct Collapsible {
26    is_open: Signal<bool>,
27    on_toggle: Option<Box<dyn Fn(&mut EventContext, bool)>>,
28}
29
30impl Collapsible {
31    /// Create a new collapsible view with a header and content.
32    pub fn new(
33        cx: &mut Context,
34        header: impl Fn(&mut Context),
35        content: impl Fn(&mut Context),
36    ) -> Handle<Self> {
37        let is_open = Signal::new(false);
38
39        Self { is_open, on_toggle: None }
40            .build(cx, |cx| {
41                let entity = cx.current();
42                // Header
43                HStack::new(cx, |cx| {
44                    header(cx);
45                    Svg::new(cx, ICON_CHEVRON_DOWN)
46                        .class("expand-icon")
47                        .on_press(|cx| cx.emit(CollapsibleEvent::ToggleOpen));
48                })
49                .navigable(true)
50                .role(Role::Button)
51                .expanded(is_open)
52                .class("header")
53                .controls(format!("{}", entity))
54                .on_press(|cx| cx.emit(CollapsibleEvent::ToggleOpen));
55
56                // Content
57                VStack::new(cx, |cx| {
58                    content(cx);
59                })
60                .id(format!("{}", entity))
61                .class("content");
62            })
63            .toggle_class("open", is_open)
64    }
65}
66
67impl View for Collapsible {
68    fn element(&self) -> Option<&'static str> {
69        Some("collapsible")
70    }
71
72    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
73        event.map(|collapsible_event, _| match collapsible_event {
74            CollapsibleEvent::ToggleOpen => {
75                self.is_open.set(!self.is_open.get());
76
77                if let Some(callback) = &self.on_toggle {
78                    (callback)(cx, self.is_open.get());
79                }
80            }
81        });
82    }
83}
84
85impl Handle<'_, Collapsible> {
86    /// Set the open state of the collapsible view.
87    pub fn open(self, open: impl Res<bool> + 'static) -> Self {
88        let open = open.to_signal(self.cx);
89        self.bind(open, move |handle| {
90            let open = open.get();
91            handle.modify(|collapsible| {
92                collapsible.is_open.set_if_changed(open);
93            });
94        })
95    }
96
97    /// Set the callback which is triggered when the open state changes.
98    pub fn on_toggle(self, callback: impl Fn(&mut EventContext, bool) + 'static) -> Self {
99        self.modify(|collapsible| collapsible.on_toggle = Some(Box::new(callback)))
100    }
101}