Skip to main content

vizia_core/views/
tabview.rs

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
221/// A panel associated with a tab index.
222///
223/// The panel content is shown when its index matches the selected index and hidden otherwise.
224pub 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    /// Set the callback triggered when the close button of the tab is pressed.
344    /// The tab close button is not displayed by default. Setting this callback causes the close button to be displayed.
345    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}