Skip to main content

tui_lipan/widgets/accordion/
mod.rs

1//! Accordion widget.
2
3mod types;
4pub use types::*;
5
6use crate::callback::Callback;
7use crate::core::element::Element;
8use crate::core::event::MouseEvent;
9use crate::style::{Align, BorderStyle, Length, Padding, Style, StyleSlot};
10use crate::widgets::{Button, Frame, VStack};
11use std::sync::Arc;
12
13/// An accordion widget.
14#[derive(Clone)]
15pub struct Accordion {
16    pub(crate) items: Vec<AccordionItem>,
17    pub(crate) on_toggle: Option<Callback<usize>>,
18    pub(crate) exclusive: bool,
19    pub(crate) gap: u16,
20    pub(crate) padding: Padding,
21    pub(crate) border: bool,
22    pub(crate) border_style: BorderStyle,
23    pub(crate) style: Style,
24    pub(crate) header_style: Style,
25    pub(crate) header_hover_style: StyleSlot,
26    pub(crate) header_focus_style: StyleSlot,
27    pub(crate) header_padding: Padding,
28    pub(crate) content_padding: Padding,
29    pub(crate) content_border: bool,
30    pub(crate) content_border_style: BorderStyle,
31    pub(crate) content_style: Style,
32    pub(crate) disabled_style: Style,
33    pub(crate) expanded_icon: Arc<str>,
34    pub(crate) collapsed_icon: Arc<str>,
35    pub(crate) focusable: bool,
36    pub(crate) tab_stop: bool,
37    pub(crate) on_focus: Option<Callback<()>>,
38    pub(crate) on_blur: Option<Callback<()>>,
39    pub(crate) width: Length,
40    pub(crate) height: Length,
41}
42
43impl Default for Accordion {
44    fn default() -> Self {
45        Self {
46            items: Vec::new(),
47            on_toggle: None,
48            exclusive: false,
49            gap: 0,
50            padding: Padding::default(),
51            border: false,
52            border_style: BorderStyle::Plain,
53            style: Style::default(),
54            header_style: Style::default(),
55            header_hover_style: StyleSlot::Inherit,
56            header_focus_style: StyleSlot::Inherit,
57            header_padding: Padding::default(),
58            content_padding: Padding::default(),
59            content_border: false,
60            content_border_style: BorderStyle::Plain,
61            content_style: Style::default(),
62            disabled_style: Style::default(),
63            expanded_icon: "▼ ".into(),
64            collapsed_icon: "▶ ".into(),
65            focusable: false,
66            tab_stop: true,
67            on_focus: None,
68            on_blur: None,
69            width: Length::Flex(1),
70            height: Length::Auto,
71        }
72    }
73}
74
75impl Accordion {
76    /// Create a new accordion.
77    pub fn new() -> Self {
78        Self::default()
79    }
80
81    /// Add a section.
82    pub fn item(mut self, item: AccordionItem) -> Self {
83        self.items.push(item);
84        self
85    }
86
87    /// Set toggle callback.
88    pub fn on_toggle(mut self, cb: Callback<usize>) -> Self {
89        self.on_toggle = Some(cb);
90        self
91    }
92
93    /// Set exclusive mode (only one section open at a time).
94    pub fn exclusive(mut self, exclusive: bool) -> Self {
95        self.exclusive = exclusive;
96        self
97    }
98
99    /// Set gap between sections.
100    pub fn gap(mut self, gap: u16) -> Self {
101        self.gap = gap;
102        self
103    }
104
105    /// Set padding around the accordion.
106    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
107        self.padding = padding.into();
108        self
109    }
110
111    /// Draw a border.
112    pub fn border(mut self, border: bool) -> Self {
113        self.border = border;
114        self
115    }
116
117    /// Set border style.
118    pub fn border_style(mut self, border_style: BorderStyle) -> Self {
119        self.border_style = border_style;
120        self
121    }
122
123    /// Set base style.
124    pub fn style(mut self, style: Style) -> Self {
125        self.style = style;
126        self
127    }
128
129    /// Set header style.
130    pub fn header_style(mut self, style: Style) -> Self {
131        self.header_style = style;
132        self
133    }
134
135    /// Set header hover style.
136    pub fn header_hover_style(mut self, style: Style) -> Self {
137        self.header_hover_style = StyleSlot::Replace(style);
138        self
139    }
140
141    /// Extend the themed header hover style.
142    pub fn extend_header_hover_style(mut self, style: Style) -> Self {
143        self.header_hover_style = StyleSlot::Extend(style);
144        self
145    }
146
147    /// Inherit the themed header hover style.
148    pub fn inherit_header_hover_style(mut self) -> Self {
149        self.header_hover_style = StyleSlot::Inherit;
150        self
151    }
152
153    /// Set header hover style slot directly for composite forwarding.
154    pub fn header_hover_style_slot(mut self, slot: StyleSlot) -> Self {
155        self.header_hover_style = slot;
156        self
157    }
158
159    /// Set header focus style.
160    pub fn header_focus_style(mut self, style: Style) -> Self {
161        self.header_focus_style = StyleSlot::Replace(style);
162        self
163    }
164
165    /// Extend the themed header focus style.
166    pub fn extend_header_focus_style(mut self, style: Style) -> Self {
167        self.header_focus_style = StyleSlot::Extend(style);
168        self
169    }
170
171    /// Inherit the themed header focus style.
172    pub fn inherit_header_focus_style(mut self) -> Self {
173        self.header_focus_style = StyleSlot::Inherit;
174        self
175    }
176
177    /// Set header focus style slot directly for composite forwarding.
178    pub fn header_focus_style_slot(mut self, slot: StyleSlot) -> Self {
179        self.header_focus_style = slot;
180        self
181    }
182
183    /// Set header padding.
184    pub fn header_padding(mut self, padding: impl Into<Padding>) -> Self {
185        self.header_padding = padding.into();
186        self
187    }
188
189    /// Set content padding.
190    pub fn content_padding(mut self, padding: impl Into<Padding>) -> Self {
191        self.content_padding = padding.into();
192        self
193    }
194
195    /// Draw a border around expanded content.
196    pub fn content_border(mut self, border: bool) -> Self {
197        self.content_border = border;
198        self
199    }
200
201    /// Set content border style.
202    pub fn content_border_style(mut self, border_style: BorderStyle) -> Self {
203        self.content_border_style = border_style;
204        self
205    }
206
207    /// Set content style.
208    pub fn content_style(mut self, style: Style) -> Self {
209        self.content_style = style;
210        self
211    }
212
213    /// Set disabled style.
214    pub fn disabled_style(mut self, style: Style) -> Self {
215        self.disabled_style = style;
216        self
217    }
218
219    /// Set expanded icon.
220    pub fn expanded_icon(mut self, icon: impl Into<Arc<str>>) -> Self {
221        self.expanded_icon = icon.into();
222        self
223    }
224
225    /// Set collapsed icon.
226    pub fn collapsed_icon(mut self, icon: impl Into<Arc<str>>) -> Self {
227        self.collapsed_icon = icon.into();
228        self
229    }
230
231    /// Control whether section headers are focusable.
232    pub fn focusable(mut self, focusable: bool) -> Self {
233        self.focusable = focusable;
234        self
235    }
236
237    /// Control whether section headers participate in tab traversal.
238    pub fn tab_stop(mut self, tab_stop: bool) -> Self {
239        self.tab_stop = tab_stop;
240        self
241    }
242
243    /// Set the callback fired when a section header gains focus.
244    pub fn on_focus(mut self, cb: Callback<()>) -> Self {
245        self.on_focus = Some(cb);
246        self
247    }
248
249    /// Set the callback fired when a section header loses focus.
250    pub fn on_blur(mut self, cb: Callback<()>) -> Self {
251        self.on_blur = Some(cb);
252        self
253    }
254
255    /// Set width.
256    pub fn width(mut self, width: Length) -> Self {
257        self.width = width;
258        self
259    }
260
261    /// Set height.
262    pub fn height(mut self, height: Length) -> Self {
263        self.height = height;
264        self
265    }
266}
267
268impl From<Accordion> for Element {
269    fn from(accordion: Accordion) -> Self {
270        let mut stack = VStack::new()
271            .gap(accordion.gap)
272            .padding(accordion.padding)
273            .border(accordion.border)
274            .border_style(accordion.border_style)
275            .style(accordion.style)
276            .width(accordion.width)
277            .height(accordion.height);
278
279        for (i, item) in accordion.items.into_iter().enumerate() {
280            let icon = if item.expanded {
281                accordion.expanded_icon.clone()
282            } else {
283                accordion.collapsed_icon.clone()
284            };
285            let title = format!("{}{}", icon, item.title);
286
287            let mut header = Button::filled(title)
288                .width(Length::Flex(1))
289                .align(Align::Start)
290                .padding(accordion.header_padding)
291                .style(accordion.header_style)
292                .hover_style_slot(accordion.header_hover_style)
293                .focus_style_slot(accordion.header_focus_style)
294                .focusable(accordion.focusable)
295                .tab_stop(accordion.tab_stop)
296                .disabled(item.disabled)
297                .disabled_style(accordion.disabled_style);
298
299            if let Some(cb) = accordion.on_focus.clone() {
300                header = header.on_focus(cb);
301            }
302            if let Some(cb) = accordion.on_blur.clone() {
303                header = header.on_blur(cb);
304            }
305
306            if let Some(cb) = accordion.on_toggle.clone()
307                && !item.disabled
308            {
309                header = header.on_click(Callback::new(move |_: MouseEvent| cb.emit(i)));
310            }
311
312            stack = stack.child(header);
313
314            if item.expanded {
315                let content = Frame::new()
316                    .border(accordion.content_border)
317                    .border_style(accordion.content_border_style)
318                    .padding(accordion.content_padding)
319                    .style(accordion.content_style)
320                    .width(Length::Flex(1))
321                    .height(Length::Auto)
322                    .child(item.content);
323                stack = stack.child(content);
324            }
325        }
326
327        stack.into()
328    }
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334
335    #[test]
336    fn accordion_headers_are_not_focusable_by_default() {
337        assert!(!Accordion::new().focusable);
338    }
339
340    #[test]
341    fn accordion_focusable_builder_updates_header_focusability() {
342        assert!(Accordion::new().focusable(true).focusable);
343    }
344}