Skip to main content

freya_components/
accordion.rs

1use freya_animation::prelude::{
2    AnimNum,
3    Ease,
4    Function,
5    use_animation,
6};
7use freya_core::prelude::*;
8use torin::{
9    gaps::Gaps,
10    prelude::VisibleSize,
11};
12
13use crate::{
14    define_theme,
15    get_theme,
16};
17
18define_theme! {
19    %[component]
20    pub Accordion {
21        %[fields]
22        color: Color,
23        background: Color,
24        border_fill: Color,
25    }
26}
27
28/// A container that expands/collapses vertically when pressed.
29///
30/// # Example
31///
32/// ```rust
33/// # use freya::prelude::*;
34/// const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna.";
35///
36/// fn app() -> impl IntoElement {
37///     rect()
38///        .center()
39///        .expanded()
40///        .spacing(4.)
41///        .children((0..2).map(|_| {
42///            Accordion::new()
43///                .header("Click to expand!")
44///                .child(LOREM_IPSUM)
45///                .into()
46///        }))
47/// }
48///
49/// # use freya_testing::prelude::*;
50/// # use std::time::Duration;
51/// # launch_doc(|| {
52/// #   rect().child(app())
53/// # }, "./images/gallery_accordion.png").with_hook(|t| {
54/// #   t.click_cursor((125., 115.));
55/// #   t.poll(Duration::from_millis(1), Duration::from_millis(300));
56/// #   t.sync_and_update();
57/// # });
58/// ```
59///
60/// # Preview
61/// ![Accordion Preview][accordion]
62#[cfg_attr(feature = "docs",
63    doc = embed_doc_image::embed_image!("accordion", "images/gallery_accordion.png")
64)]
65#[derive(Clone, PartialEq, Default)]
66pub struct Accordion {
67    pub(crate) theme: Option<AccordionThemePartial>,
68    header: Option<Element>,
69    children: Vec<Element>,
70    cursor_icon: CursorIcon,
71    key: DiffKey,
72}
73
74impl KeyExt for Accordion {
75    fn write_key(&mut self) -> &mut DiffKey {
76        &mut self.key
77    }
78}
79
80impl Accordion {
81    pub fn new() -> Self {
82        Self::default()
83    }
84
85    pub fn header<C: Into<Element>>(mut self, header: C) -> Self {
86        self.header = Some(header.into());
87        self
88    }
89
90    /// Override the cursor icon shown when hovering over this component.
91    pub fn cursor_icon(mut self, cursor_icon: impl Into<CursorIcon>) -> Self {
92        self.cursor_icon = cursor_icon.into();
93        self
94    }
95}
96
97impl ChildrenExt for Accordion {
98    fn get_children(&mut self) -> &mut Vec<Element> {
99        &mut self.children
100    }
101}
102
103impl Component for Accordion {
104    fn render(self: &Accordion) -> impl IntoElement {
105        let header_a11y_id = use_a11y();
106        let accordion_theme = get_theme!(&self.theme, AccordionThemePreference, "accordion");
107        let cursor_icon = self.cursor_icon;
108        let mut open = use_state(|| false);
109        let mut animation = use_animation(move |_conf| {
110            AnimNum::new(0., 100.)
111                .time(300)
112                .function(Function::Expo)
113                .ease(Ease::Out)
114        });
115
116        let clip_percent = animation.get().value();
117
118        rect()
119            .a11y_id(header_a11y_id)
120            .a11y_role(AccessibilityRole::Header)
121            .a11y_focusable(true)
122            .corner_radius(CornerRadius::new_all(8.))
123            .padding(Gaps::new_all(8.))
124            .color(accordion_theme.color)
125            .background(accordion_theme.background)
126            .border(
127                Border::new()
128                    .fill(accordion_theme.border_fill)
129                    .width(1.)
130                    .alignment(BorderAlignment::Inner),
131            )
132            .on_pointer_enter(move |_| {
133                Cursor::set(cursor_icon);
134            })
135            .on_pointer_leave(move |_| {
136                Cursor::set(CursorIcon::default());
137            })
138            .on_press(move |_| {
139                if open.toggled() {
140                    animation.start();
141                } else {
142                    animation.reverse();
143                }
144            })
145            .maybe_child(self.header.clone())
146            .child(
147                rect()
148                    .a11y_role(AccessibilityRole::Region)
149                    .a11y_builder(|b| {
150                        b.set_labelled_by([header_a11y_id]);
151                        if !open() {
152                            b.set_hidden();
153                        }
154                    })
155                    .overflow(Overflow::Clip)
156                    .visible_height(VisibleSize::inner_percent(clip_percent))
157                    .children(self.children.clone()),
158            )
159    }
160
161    fn render_key(&self) -> DiffKey {
162        self.key.clone().or(self.default_key())
163    }
164}