1use std::ops::Deref;
2use std::sync::Arc;
3
4use crate::icons::ICON_X;
5use crate::prelude::*;
6
7pub enum TabListEvent {
8 SetSelected(usize),
9 CloseFocused,
10 RequestClose(usize),
11 SetTabListName(String),
12 SetTabListLabeledBy(String),
13}
14
15pub struct TabView {
16 selected_index: Signal<usize>,
17 is_vertical: Signal<bool>,
18 tablist_name: Signal<String>,
19 tablist_labeled_by: Signal<Option<String>>,
20 on_select: Option<Box<dyn Fn(&mut EventContext, usize)>>,
21 on_close: Option<Box<dyn Fn(&mut EventContext, usize)>>,
22}
23
24impl TabView {
25 pub fn new<S, V, T, F>(cx: &mut Context, list: S, content: F) -> Handle<Self>
26 where
27 S: Res<V> + 'static,
28 V: Deref<Target = [T]> + Clone + 'static,
29 T: PartialEq + Clone + 'static,
30 F: 'static + Clone + Fn(&mut Context, usize, T) -> TabPair,
31 {
32 let selected_index = Signal::new(0usize);
33 let is_vertical = Signal::new(false);
34 let tablist_name = Signal::new(String::from("Tabs"));
35 let tablist_labeled_by = Signal::new(None::<String>);
36 let list = list.to_signal(cx);
37
38 Self {
39 selected_index,
40 is_vertical,
41 tablist_name,
42 tablist_labeled_by,
43 on_select: None,
44 on_close: None,
45 }
46 .build(cx, move |cx| {
47 let tabview_entity = cx.current();
48 let content_for_headers = content.clone();
49
50 let tablist_entity = TabList::new(cx, list, move |cx, index, item, is_selected| {
51 let tab_id = format!("{}-tab-{}", tabview_entity, index);
52 let panel_id = format!("{}-panel-{}", tabview_entity, index);
53 let tab_pair = (content_for_headers)(cx, index, item);
54 let builder = tab_pair.header;
55 let menu = tab_pair.menu;
56 let closeable = tab_pair.closeable;
57
58 let mut tab = Tab::with_content(cx, index, builder)
59 .id(tab_id)
60 .controls(panel_id)
61 .checked(is_selected)
62 .focused(is_selected)
63 .selected(is_selected)
64 .toggle_class("vertical", is_vertical);
65
66 if let Some(menu_builder) = menu {
67 tab = tab.menu(menu_builder);
68 }
69
70 if closeable {
71 tab = tab.on_close(move |cx| {
72 cx.emit_to(tabview_entity, TabListEvent::RequestClose(index));
73 });
74 }
75
76 tab
77 })
78 .vertical(is_vertical)
79 .orientation(is_vertical.map(|vertical| {
80 if *vertical { Orientation::Vertical } else { Orientation::Horizontal }
81 }))
82 .name(tablist_name)
83 .selection(selected_index)
84 .on_select(move |cx, index| {
85 cx.emit_to(tabview_entity, TabListEvent::SetSelected(index))
86 })
87 .on_close(move |cx, index| {
88 cx.emit_to(tabview_entity, TabListEvent::RequestClose(index))
89 })
90 .toggle_class("vertical", is_vertical)
91 .entity();
92
93 Binding::new(cx, tablist_labeled_by, move |cx| {
94 if let Some(label_id) = tablist_labeled_by.get() {
95 cx.style.labelled_by.insert(tablist_entity, label_id);
96 cx.style.needs_access_update(tablist_entity);
97 }
98 });
99
100 VStack::new(cx, move |cx| {
101 Binding::new(cx, list, move |cx| {
102 let list_values = list.get();
103 let content_for_panels = content.clone();
104
105 for (index, item) in list_values.iter().cloned().enumerate() {
106 let content_for_panel = content_for_panels.clone();
107 let tab_id = format!("{}-tab-{}", tabview_entity, index);
108 let panel_id = format!("{}-panel-{}", tabview_entity, index);
109
110 TabPanel::new(cx, index, selected_index, move |cx| {
111 ((content_for_panel)(cx, index, item.clone()).content)(cx);
112 })
113 .id(panel_id)
114 .role(Role::TabPanel)
115 .labeled_by(tab_id)
116 .hidden(selected_index.map(move |selected| *selected != index));
117 }
118 });
119 })
120 .overflow(Overflow::Hidden)
121 .class("tabview-content-wrapper");
122 })
123 .toggle_class("vertical", is_vertical)
124 }
125}
126
127impl View for TabView {
128 fn element(&self) -> Option<&'static str> {
129 Some("tabview")
130 }
131
132 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
133 event.map(|tab_event, meta| match tab_event {
134 TabListEvent::SetSelected(index) => {
135 if self.selected_index.get() != *index {
136 self.selected_index.set(*index);
137 if let Some(callback) = &self.on_select {
138 (callback)(cx, *index);
139 }
140 }
141 meta.consume();
142 }
143
144 TabListEvent::CloseFocused => {}
145
146 TabListEvent::RequestClose(index) => {
147 if let Some(callback) = &self.on_close {
148 (callback)(cx, *index);
149 }
150 meta.consume();
151 }
152
153 TabListEvent::SetTabListName(name) => {
154 self.tablist_name.set_if_changed(name.clone());
155 meta.consume();
156 }
157
158 TabListEvent::SetTabListLabeledBy(id) => {
159 self.tablist_labeled_by.set_if_changed(Some(id.clone()));
160 meta.consume();
161 }
162 });
163 }
164}
165
166impl Handle<'_, TabView> {
167 pub fn vertical(self) -> Self {
168 self.modify(|tabview: &mut TabView| {
169 tabview.is_vertical.set(true);
170 })
171 }
172
173 pub fn on_select(self, callback: impl Fn(&mut EventContext, usize) + 'static) -> Self {
174 self.modify(|tabview: &mut TabView| tabview.on_select = Some(Box::new(callback)))
175 }
176
177 pub fn on_close(self, callback: impl Fn(&mut EventContext, usize) + 'static) -> Self {
178 self.modify(|tabview: &mut TabView| tabview.on_close = Some(Box::new(callback)))
179 }
180
181 pub fn tablist_name<U>(mut self, name: impl Res<U>) -> Self
182 where
183 U: ToStringLocalized + 'static,
184 {
185 name.set_or_bind(self.context(), |cx, name| {
186 let name = name.get_value(cx).to_string_local(cx);
187 cx.emit(TabListEvent::SetTabListName(name));
188 });
189
190 self
191 }
192
193 pub fn tablist_labeled_by<U>(mut self, id: impl Res<U>) -> Self
194 where
195 U: Into<String> + Clone + 'static,
196 {
197 id.set_or_bind(self.context(), |cx, id| {
198 cx.emit(TabListEvent::SetTabListLabeledBy(id.get_value(cx).into()));
199 });
200
201 self
202 }
203
204 pub fn with_selected<U: Into<usize> + Clone + 'static>(
205 mut self,
206 selected: impl Res<U> + 'static,
207 ) -> Self {
208 let _entity = self.entity();
209 let selected = selected.to_signal(self.context());
210 self.bind(selected, move |handle| {
211 let index = selected.get().into();
212 handle.modify(|tabview: &mut TabView| {
213 tabview.selected_index.set(index);
214 });
215 })
216 }
217}
218
219pub struct TabPanel {}
223
224impl TabPanel {
225 pub fn new<U, F>(
226 cx: &mut Context,
227 index: usize,
228 selected_index: impl Res<U> + 'static,
229 content: F,
230 ) -> Handle<Self>
231 where
232 U: Into<usize> + Clone + 'static,
233 F: 'static + Fn(&mut Context),
234 {
235 let selected_index = selected_index.to_signal(cx);
236
237 Self {}
238 .build(cx, move |cx| {
239 (content)(cx);
240 })
241 .display(selected_index.map(move |selected| {
242 if selected.clone().into() == index { Display::Flex } else { Display::None }
243 }))
244 }
245}
246
247impl View for TabPanel {
248 fn element(&self) -> Option<&'static str> {
249 Some("tabpanel")
250 }
251}
252
253pub struct TabPair {
254 pub header: Box<dyn Fn(&mut Context)>,
255 pub content: Box<dyn Fn(&mut Context)>,
256 pub menu: Option<Box<dyn for<'a> Fn(&'a mut Context) -> Handle<'a, Popover>>>,
257 pub closeable: bool,
258}
259
260impl TabPair {
261 pub fn new<H, C>(header: H, content: C) -> Self
262 where
263 H: 'static + Fn(&mut Context),
264 C: 'static + Fn(&mut Context),
265 {
266 Self { header: Box::new(header), content: Box::new(content), menu: None, closeable: false }
267 }
268
269 pub fn menu<M>(mut self, menu: M) -> Self
270 where
271 M: 'static + for<'a> Fn(&'a mut Context) -> Handle<'a, Popover>,
272 {
273 self.menu = Some(Box::new(menu));
274 self
275 }
276
277 pub fn closeable(mut self, closeable: bool) -> Self {
278 self.closeable = closeable;
279 self
280 }
281}
282
283pub struct Tab {
284 on_close: Option<Arc<dyn Fn(&mut EventContext) + Send + Sync>>,
285 has_close: Signal<bool>,
286}
287
288impl Tab {
289 pub fn with_content<F>(cx: &mut Context, index: usize, content: F) -> Handle<Self>
290 where
291 F: 'static + Fn(&mut Context),
292 {
293 let has_close = Signal::new(false);
294
295 Self { on_close: None, has_close }
296 .build(cx, move |cx| {
297 (content)(cx);
298
299 Binding::new(cx, has_close, move |cx| {
300 if has_close.get() {
301 let on_close = cx.data::<Tab>().on_close.clone().unwrap();
302 Button::new(cx, |cx| Svg::new(cx, ICON_X))
303 .class("close")
304 .variant(ButtonVariant::Text)
305 .navigable(false)
306 .focusable(true)
307 .on_press(move |cx| (on_close)(cx));
308 }
309 });
310 })
311 .role(Role::Tab)
312 .navigable(false)
313 .focusable(false)
314 .toggle_class("closeable", has_close)
315 .layout_type(LayoutType::Row)
316 .on_press(move |cx| {
317 cx.emit(ListEvent::Select(index));
318 })
319 }
320
321 pub fn new<T: ToStringLocalized + 'static>(
322 cx: &mut Context,
323 index: usize,
324 label: impl Res<T> + Clone + 'static,
325 selected: impl Res<bool> + 'static,
326 ) -> Handle<Self> {
327 Self::with_content(cx, index, move |cx| {
328 Label::new(cx, label.clone()).hoverable(false);
329 })
330 .checked(selected)
331 }
332}
333
334impl View for Tab {
335 fn element(&self) -> Option<&'static str> {
336 Some("tab")
337 }
338}
339
340impl Handle<'_, Tab> {
341 pub fn on_close(self, callback: impl Fn(&mut EventContext) + 'static + Send + Sync) -> Self {
344 self.modify(|tab: &mut Tab| {
345 tab.on_close = Some(Arc::new(callback));
346 tab.has_close.set(true);
347 })
348 }
349}
350
351pub struct TabList {
352 is_vertical: Signal<bool>,
353 selected_index: Signal<Option<usize>>,
354 selected_indices: Signal<Vec<usize>>,
355 on_select: Option<Box<dyn Fn(&mut EventContext, usize)>>,
356 on_close: Option<Box<dyn Fn(&mut EventContext, usize)>>,
357}
358
359impl TabList {
360 pub fn new<S, V, T, F, H>(cx: &mut Context, list: S, item_content: F) -> Handle<Self>
361 where
362 S: Res<V> + 'static,
363 V: Deref<Target = [T]> + Clone + 'static,
364 T: PartialEq + Clone + 'static,
365 F: 'static + Clone + for<'a> Fn(&'a mut Context, usize, T, Memo<bool>) -> Handle<'a, H>,
366 H: View,
367 {
368 let is_vertical = Signal::new(false);
369 let selected_index = Signal::new(Some(0));
370 let selected_indices = Signal::new(vec![0usize]);
371
372 Self { is_vertical, selected_index, selected_indices, on_select: None, on_close: None }
373 .build(cx, move |cx| {
374 let item_content = item_content.clone();
375 let selected_indices = selected_indices;
376 let list_entity = List::new_custom_items_with_selection(
377 cx,
378 list,
379 move |cx, index, item, is_selected| {
380 let is_selected_for_scroll = is_selected;
381 (item_content)(cx, index, item.get(), is_selected)
382 .bind(is_selected_for_scroll, move |handle| {
383 if is_selected_for_scroll.get() {
384 handle.cx.emit(ScrollEvent::ScrollToView(handle.entity()));
385 }
386 })
387 .on_geo_changed(move |cx, geo| {
388 if is_selected_for_scroll.get()
389 && geo.intersects(GeoChanged::WIDTH_CHANGED)
390 {
391 cx.emit(ScrollEvent::ScrollToView(cx.current()));
392 }
393 })
394 },
395 )
396 .horizontal(is_vertical.map(|vertical| !*vertical))
397 .selectable(Selectable::Single)
398 .min_selected(1)
399 .selection(selected_indices)
400 .selection_follows_focus(true)
401 .on_select(|cx, index| cx.emit(TabListEvent::SetSelected(index)))
402 .role(Role::TabList)
403 .show_horizontal_scrollbar(is_vertical.map(|vertical| !*vertical))
404 .show_vertical_scrollbar(is_vertical.map(|vertical| *vertical))
405 .entity();
406
407 cx.with_current(list_entity, |cx| {
408 Keymap::from(vec![(
409 KeyChord::new(Modifiers::empty(), Code::Delete),
410 KeymapEntry::new("Close Focused Tab", |cx| {
411 cx.emit(TabListEvent::CloseFocused)
412 }),
413 )])
414 .build(cx);
415 });
416 })
417 .toggle_class("vertical", is_vertical)
418 }
419}
420
421impl Handle<'_, TabList> {
422 pub fn selection<U: Into<usize> + Clone + 'static>(
423 self,
424 selected: impl Res<U> + 'static,
425 ) -> Self {
426 let selected = selected.to_signal(self.cx);
427 self.bind(selected, move |handle| {
428 let index = selected.get().into();
429 handle.modify(|tablist: &mut TabList| {
430 tablist.selected_indices.set(vec![index]);
431 tablist.selected_index.set(Some(index));
432 });
433 })
434 }
435
436 pub fn vertical(self, vertical: impl Res<bool> + 'static) -> Self {
437 let vertical = vertical.to_signal(self.cx);
438 self.bind(vertical, move |handle| {
439 let vertical = vertical.get();
440 handle.modify(|tablist: &mut TabList| tablist.is_vertical.set(vertical));
441 })
442 }
443
444 pub fn on_select(self, callback: impl Fn(&mut EventContext, usize) + 'static) -> Self {
445 self.modify(|tablist: &mut TabList| tablist.on_select = Some(Box::new(callback)))
446 }
447
448 pub fn on_close(self, callback: impl Fn(&mut EventContext, usize) + 'static) -> Self {
449 self.modify(|tablist: &mut TabList| tablist.on_close = Some(Box::new(callback)))
450 }
451}
452
453impl View for TabList {
454 fn element(&self) -> Option<&'static str> {
455 Some("tablist")
456 }
457
458 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
459 event.map(|tab_list_event, meta| match tab_list_event {
460 TabListEvent::SetSelected(index) => {
461 self.selected_indices.set(vec![*index]);
462 self.selected_index.set(Some(*index));
463 if let Some(callback) = &self.on_select {
464 (callback)(cx, *index);
465 }
466 meta.consume();
467 }
468
469 TabListEvent::CloseFocused => {
470 if let Some(index) = self.selected_index.get() {
471 if let Some(callback) = &self.on_close {
472 (callback)(cx, index);
473 }
474 }
475 meta.consume();
476 }
477
478 TabListEvent::RequestClose(_) => {}
479
480 TabListEvent::SetTabListName(_) => {}
481
482 TabListEvent::SetTabListLabeledBy(_) => {}
483 });
484 }
485}