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