tui_lipan/widgets/accordion/
mod.rs1mod 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#[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 pub fn new() -> Self {
78 Self::default()
79 }
80
81 pub fn item(mut self, item: AccordionItem) -> Self {
83 self.items.push(item);
84 self
85 }
86
87 pub fn on_toggle(mut self, cb: Callback<usize>) -> Self {
89 self.on_toggle = Some(cb);
90 self
91 }
92
93 pub fn exclusive(mut self, exclusive: bool) -> Self {
95 self.exclusive = exclusive;
96 self
97 }
98
99 pub fn gap(mut self, gap: u16) -> Self {
101 self.gap = gap;
102 self
103 }
104
105 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
107 self.padding = padding.into();
108 self
109 }
110
111 pub fn border(mut self, border: bool) -> Self {
113 self.border = border;
114 self
115 }
116
117 pub fn border_style(mut self, border_style: BorderStyle) -> Self {
119 self.border_style = border_style;
120 self
121 }
122
123 pub fn style(mut self, style: Style) -> Self {
125 self.style = style;
126 self
127 }
128
129 pub fn header_style(mut self, style: Style) -> Self {
131 self.header_style = style;
132 self
133 }
134
135 pub fn header_hover_style(mut self, style: Style) -> Self {
137 self.header_hover_style = StyleSlot::Replace(style);
138 self
139 }
140
141 pub fn extend_header_hover_style(mut self, style: Style) -> Self {
143 self.header_hover_style = StyleSlot::Extend(style);
144 self
145 }
146
147 pub fn inherit_header_hover_style(mut self) -> Self {
149 self.header_hover_style = StyleSlot::Inherit;
150 self
151 }
152
153 pub fn header_hover_style_slot(mut self, slot: StyleSlot) -> Self {
155 self.header_hover_style = slot;
156 self
157 }
158
159 pub fn header_focus_style(mut self, style: Style) -> Self {
161 self.header_focus_style = StyleSlot::Replace(style);
162 self
163 }
164
165 pub fn extend_header_focus_style(mut self, style: Style) -> Self {
167 self.header_focus_style = StyleSlot::Extend(style);
168 self
169 }
170
171 pub fn inherit_header_focus_style(mut self) -> Self {
173 self.header_focus_style = StyleSlot::Inherit;
174 self
175 }
176
177 pub fn header_focus_style_slot(mut self, slot: StyleSlot) -> Self {
179 self.header_focus_style = slot;
180 self
181 }
182
183 pub fn header_padding(mut self, padding: impl Into<Padding>) -> Self {
185 self.header_padding = padding.into();
186 self
187 }
188
189 pub fn content_padding(mut self, padding: impl Into<Padding>) -> Self {
191 self.content_padding = padding.into();
192 self
193 }
194
195 pub fn content_border(mut self, border: bool) -> Self {
197 self.content_border = border;
198 self
199 }
200
201 pub fn content_border_style(mut self, border_style: BorderStyle) -> Self {
203 self.content_border_style = border_style;
204 self
205 }
206
207 pub fn content_style(mut self, style: Style) -> Self {
209 self.content_style = style;
210 self
211 }
212
213 pub fn disabled_style(mut self, style: Style) -> Self {
215 self.disabled_style = style;
216 self
217 }
218
219 pub fn expanded_icon(mut self, icon: impl Into<Arc<str>>) -> Self {
221 self.expanded_icon = icon.into();
222 self
223 }
224
225 pub fn collapsed_icon(mut self, icon: impl Into<Arc<str>>) -> Self {
227 self.collapsed_icon = icon.into();
228 self
229 }
230
231 pub fn focusable(mut self, focusable: bool) -> Self {
233 self.focusable = focusable;
234 self
235 }
236
237 pub fn tab_stop(mut self, tab_stop: bool) -> Self {
239 self.tab_stop = tab_stop;
240 self
241 }
242
243 pub fn on_focus(mut self, cb: Callback<()>) -> Self {
245 self.on_focus = Some(cb);
246 self
247 }
248
249 pub fn on_blur(mut self, cb: Callback<()>) -> Self {
251 self.on_blur = Some(cb);
252 self
253 }
254
255 pub fn width(mut self, width: Length) -> Self {
257 self.width = width;
258 self
259 }
260
261 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}