1use std::ops::Deref;
2
3use crate::prelude::*;
4
5pub enum AccordionEvent {
6 ToggleOpen(usize, bool),
7 ClearHeaders,
8 RegisterHeader(usize, Entity),
9 FocusNextHeader,
10 FocusPrevHeader,
11 FocusFirstHeader,
12 FocusLastHeader,
13}
14
15pub struct Accordion {
20 open_indices: Signal<Vec<usize>>,
21 on_toggle: Option<Box<dyn Fn(&mut EventContext, usize, bool) + 'static>>,
22 header_entities: Vec<Entity>,
23}
24
25impl Accordion {
26 pub fn new<S, V, T, F>(cx: &mut Context, list: S, content: F) -> Handle<Self>
28 where
29 S: Res<V> + 'static,
30 V: Deref<Target = [T]> + Clone + 'static,
31 T: Clone + 'static,
32 F: 'static + Clone + Fn(&mut Context, usize, T) -> AccordionPair,
33 {
34 let list = list.to_signal(cx);
35 let open_indices = Signal::new(Vec::new());
36
37 Self { open_indices, on_toggle: None, header_entities: Vec::new() }.build(cx, move |cx| {
38 Keymap::from(vec![
39 (
40 KeyChord::new(Modifiers::empty(), Code::ArrowDown),
41 KeymapEntry::new("Accordion Focus Next", |cx| {
42 cx.emit(AccordionEvent::FocusNextHeader)
43 }),
44 ),
45 (
46 KeyChord::new(Modifiers::empty(), Code::ArrowUp),
47 KeymapEntry::new("Accordion Focus Previous", |cx| {
48 cx.emit(AccordionEvent::FocusPrevHeader)
49 }),
50 ),
51 (
52 KeyChord::new(Modifiers::empty(), Code::Home),
53 KeymapEntry::new("Accordion Focus First", |cx| {
54 cx.emit(AccordionEvent::FocusFirstHeader)
55 }),
56 ),
57 (
58 KeyChord::new(Modifiers::empty(), Code::End),
59 KeymapEntry::new("Accordion Focus Last", |cx| {
60 cx.emit(AccordionEvent::FocusLastHeader)
61 }),
62 ),
63 ])
64 .build(cx);
65
66 Binding::new(cx, list, move |cx| {
67 let list_values = list.get();
68 let content = content.clone();
69 let list_length = list.with(|list| list.len());
70
71 cx.emit(AccordionEvent::ClearHeaders);
72
73 for (index, item) in list_values.iter().cloned().enumerate() {
74 let pair = (content)(cx, index, item);
75
76 Collapsible::new(cx, pair.header, pair.content)
77 .on_build(move |cx| {
78 if let Some(header) = cx.nth_child(0) {
79 cx.emit(AccordionEvent::RegisterHeader(index, header));
80 }
81 })
82 .on_toggle(move |cx, next_open| {
83 cx.emit(AccordionEvent::ToggleOpen(index, next_open));
84 })
85 .open(open_indices.map(move |indices| indices.contains(&index)));
86
87 if index < list_length - 1 {
88 Divider::horizontal(cx);
89 }
90 }
91 });
92 })
93 }
94}
95
96impl View for Accordion {
97 fn element(&self) -> Option<&'static str> {
98 Some("accordion")
99 }
100
101 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
102 event.map(|accordion_event, _| match accordion_event {
103 AccordionEvent::ToggleOpen(index, next_open) => {
104 if let Some(callback) = &self.on_toggle {
105 (callback)(cx, *index, *next_open);
106 }
107 }
108
109 AccordionEvent::ClearHeaders => {
110 self.header_entities.clear();
111 }
112
113 AccordionEvent::RegisterHeader(index, entity) => {
114 if self.header_entities.len() <= *index {
115 self.header_entities.resize(*index + 1, Entity::null());
116 }
117
118 self.header_entities[*index] = *entity;
119 }
120
121 AccordionEvent::FocusNextHeader => {
122 if let Some(index) = self.focused_header_index(cx) {
123 let next_index = (index + 1) % self.header_entities.len();
124 let next_header = self.header_entities[next_index];
125 cx.with_current(next_header, |cx| cx.focus());
126 }
127 }
128
129 AccordionEvent::FocusPrevHeader => {
130 if let Some(index) = self.focused_header_index(cx) {
131 let prev_index =
132 if index == 0 { self.header_entities.len() - 1 } else { index - 1 };
133 let prev_header = self.header_entities[prev_index];
134 cx.with_current(prev_header, |cx| cx.focus());
135 }
136 }
137
138 AccordionEvent::FocusFirstHeader => {
139 if let Some(first_header) = self.header_entities.first().copied() {
140 cx.with_current(first_header, |cx| cx.focus());
141 }
142 }
143
144 AccordionEvent::FocusLastHeader => {
145 if let Some(last_header) = self.header_entities.last().copied() {
146 cx.with_current(last_header, |cx| cx.focus());
147 }
148 }
149 });
150
151 event.map(|window_event, meta| match window_event {
152 WindowEvent::KeyDown(code, _) => match code {
153 Code::ArrowDown | Code::ArrowUp | Code::Home | Code::End => {
154 if self.focused_header_index(cx).is_some() {
155 meta.consume();
156 }
157 }
158 _ => {}
159 },
160 _ => {}
161 });
162 }
163}
164
165impl Accordion {
166 fn focused_header_index(&self, cx: &EventContext) -> Option<usize> {
167 let focused = cx.focused();
168 self.header_entities.iter().position(|header| {
169 !header.is_null() && (focused == *header || focused.is_descendant_of(cx.tree, *header))
170 })
171 }
172}
173
174impl Handle<'_, Accordion> {
175 pub fn open(mut self, indices: impl Res<Vec<usize>> + 'static) -> Self {
177 let indices = indices.to_signal(self.context());
178 self.bind(indices, move |handle| {
179 handle.modify(|accordion| {
180 accordion.open_indices.set(indices.get());
181 });
182 })
183 }
184
185 pub fn on_toggle<F>(self, callback: F) -> Self
190 where
191 F: 'static + Fn(&mut EventContext, usize, bool),
192 {
193 self.modify(|accordion| {
194 accordion.on_toggle = Some(Box::new(callback));
195 })
196 }
197}
198
199pub struct AccordionPair {
200 pub header: Box<dyn Fn(&mut Context)>,
201 pub content: Box<dyn Fn(&mut Context)>,
202}
203
204impl AccordionPair {
205 pub fn new<H, C>(header: H, content: C) -> Self
206 where
207 H: 'static + Fn(&mut Context),
208 C: 'static + Fn(&mut Context),
209 {
210 Self { header: Box::new(header), content: Box::new(content) }
211 }
212}