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