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