Skip to main content

floem/views/
tab.rs

1#![deny(missing_docs)]
2use std::{hash::Hash, marker::PhantomData};
3
4use floem_reactive::{Effect, Scope};
5use smallvec::SmallVec;
6
7use crate::{
8    context::{StyleCx, UpdateCx},
9    style_class,
10    view::ViewId,
11    view::{IntoView, View},
12};
13
14use super::{Diff, DiffOpAdd, FxIndexSet, HashRun, apply_diff, diff};
15
16type ViewFn<T> = Box<dyn Fn(T) -> (Box<dyn View>, Scope)>;
17
18style_class!(
19    /// Set class to TabSelector.
20    pub TabSelectorClass
21);
22
23enum TabState<V> {
24    Diff(Box<Diff<V>>),
25    Active(usize),
26    None,
27}
28
29/// Tab widget.
30///
31/// See [tab] for examples.
32pub struct Tab<T>
33where
34    T: 'static,
35{
36    id: ViewId,
37    active: Option<usize>,
38    children: Vec<Option<(ViewId, Scope)>>,
39    view_fn: ViewFn<T>,
40    phatom: PhantomData<T>,
41}
42
43/// A tab widget. Create tabs from static or dynamic lists.
44///
45/// ### Simple example
46/// ```rust
47/// # use floem::prelude::*;
48/// # use floem::theme;
49/// // Tabs from static list:
50/// let tabs = RwSignal::new(vec!["tab1, tab2, tab3"]);
51/// let active_tab = RwSignal::new(0);
52///
53/// let side_bar = tabs
54///     .get()
55///     .into_iter()
56///     .enumerate()
57///     .map(move |(idx, item)| {
58///         item.style(move |s| s
59///             .height(36.)
60///             .apply_if(idx != active_tab.get(), |s| s.apply(theme::hover_style()))
61///         )
62///     })
63///     .list()
64///     .on_select(move |idx| {
65///         if let Some(idx) = idx {
66///             active_tab.set(idx);
67///         }
68/// });
69///
70/// let static_tabs = tab(
71///     move || Some(active_tab.get()),
72///     move || tabs.get(),
73///     |it| *it,
74///     |tab_content| tab_content
75///         .container()
76///         .style(|s| s.size(150., 150.).padding(10.))
77/// );
78///
79/// stack((side_bar, static_tabs));
80/// ```
81/// ### Complex example
82/// ```rust
83/// # use floem::prelude::*;
84/// # use floem::theme;
85/// # use floem::theme::StyleThemeExt;
86/// # use floem_reactive::create_effect;
87/// // Tabs from dynamic list
88/// #[derive(Clone)]
89/// struct TabContent {
90///     idx: usize,
91///     name: String,
92/// }
93///
94/// impl TabContent {
95///     fn new(tabs_count: usize) -> Self {
96///         Self {
97///             idx: tabs_count,
98///             name: format!("Tab with index"),
99///         }
100///     }
101/// }
102///
103/// #[derive(Clone)]
104/// enum Action {
105///     Add,
106///     Remove,
107///     None,
108/// }
109/// let tabs = RwSignal::new(vec![]);
110/// let active_tab = RwSignal::new(None::<usize>);
111/// let tab_action = RwSignal::new(Action::None);
112/// create_effect(move |_| match tab_action.get() {
113///     Action::Add => {
114///         tabs.update(|tabs| tabs.push(TabContent::new(tabs.len())));
115///     }
116///     Action::Remove => {
117///         tabs.update(|tabs| { tabs.pop(); });
118///     }
119///     Action::None => ()
120/// });///
121/// let tabs_view = stack((dyn_stack(
122///     move || tabs.get(),
123///     |tab| tab.idx,
124///     move |tab| {
125///         text(format!("{} {}", tab.name, tab.idx)).button().style(move |s| s
126///             .width_full()
127///             .height(36.px())
128///             .apply_if(active_tab.get().is_some_and(|a| a == tab.idx), |s| {
129///                 s.with_theme(|s, t| s.border_color(t.primary()))
130///             })
131///         )
132///         .on_click_stop(move |_| {
133///             active_tab.update(|a| {
134///                 *a = Some(tab.idx);
135///             });
136///         })
137///     },
138/// )
139/// .style(|s| s.flex_col().width_full().row_gap(5.))
140/// .scroll()
141/// .on_click_stop(move |_| {
142///     if active_tab.with_untracked(|act| act.is_some()) {
143///         active_tab.set(None)
144///     }
145/// })
146/// .style(|s| s.size_full().padding(5.).padding_right(7.))
147/// .scroll_style(|s| s.handle_thickness(6.).shrink_to_fit()),))
148/// .style(|s| s
149///     .width(140.)
150///     .min_width(140.)
151///     .height_full()
152///     .border_right(1.)
153///     .with_theme(|s, t| s.border_color(t.border_muted()))
154/// );
155/// let tabs_content_view = stack((
156///     tab(
157///         move || active_tab.get(),
158///         move || tabs.get(),
159///         |tab| tab.idx,
160///         move |tab| {
161///          v_stack((
162///             label(move || format!("{}", tab.name)).style(|s| s
163///                 .font_size(15.)
164///                 .font_bold()),
165///             label(move || format!("{}", tab.idx)).style(|s| s
166///                 .font_size(20.)
167///                 .font_bold()),
168///             label(move || "is now active").style(|s| s
169///                 .font_size(13.)),
170///         )).style(|s| s
171///             .size(150.px(), 150.px())
172///             .items_center()
173///             .justify_center()
174///             .row_gap(10.))
175///         },
176///     ).style(|s| s.size_full()),
177/// ))
178/// .style(|s| s.size_full());
179/// ```
180pub fn tab<IF, I, T, KF, K, VF, V>(
181    active_fn: impl Fn() -> Option<usize> + 'static,
182    each_fn: IF,
183    key_fn: KF,
184    view_fn: VF,
185) -> Tab<T>
186where
187    IF: Fn() -> I + 'static,
188    I: IntoIterator<Item = T>,
189    KF: Fn(&T) -> K + 'static,
190    K: Eq + Hash + 'static,
191    VF: Fn(T) -> V + 'static,
192    V: IntoView + 'static,
193    T: 'static,
194{
195    let id = ViewId::new();
196
197    Effect::new(move |prev_hash_run| {
198        let items = each_fn();
199        let items = items.into_iter().collect::<SmallVec<[_; 128]>>();
200        let hashed_items = items.iter().map(&key_fn).collect::<FxIndexSet<_>>();
201        let diff = if let Some(HashRun(prev_hash_run)) = prev_hash_run {
202            let mut cmds = diff(&prev_hash_run, &hashed_items);
203            let mut items = items
204                .into_iter()
205                .map(|i| Some(i))
206                .collect::<SmallVec<[Option<_>; 128]>>();
207            for added in &mut cmds.added {
208                added.view = Some(items[added.at].take().unwrap());
209            }
210            cmds
211        } else {
212            let mut diff = Diff::default();
213            for (i, item) in each_fn().into_iter().enumerate() {
214                diff.added.push(DiffOpAdd {
215                    at: i,
216                    view: Some(item),
217                });
218            }
219            diff
220        };
221        id.update_state(TabState::Diff(Box::new(diff)));
222        HashRun(hashed_items)
223    });
224
225    Effect::new(move |_| {
226        let active = active_fn();
227        match active {
228            Some(idx) => id.update_state(TabState::Active::<T>(idx)),
229            None => id.update_state(TabState::None::<T>),
230        }
231    });
232
233    let view_fn = Box::new(Scope::current().enter_child(move |e| view_fn(e).into_any()));
234
235    Tab {
236        id,
237        active: None,
238        children: Vec::new(),
239        view_fn,
240        phatom: PhantomData,
241    }
242}
243
244impl<T> View for Tab<T> {
245    fn id(&self) -> ViewId {
246        self.id
247    }
248
249    fn debug_name(&self) -> std::borrow::Cow<'static, str> {
250        format!("Tab: {:?}", self.active).into()
251    }
252
253    fn update(&mut self, cx: &mut UpdateCx, state: Box<dyn std::any::Any>) {
254        if let Ok(state) = state.downcast::<TabState<T>>() {
255            match *state {
256                TabState::Diff(diff) => {
257                    apply_diff(
258                        self.id(),
259                        cx.window_state,
260                        *diff,
261                        &mut self.children,
262                        &self.view_fn,
263                    );
264                }
265                TabState::Active(active) => {
266                    self.active.replace(active);
267                    self.id.request_style_recursive();
268                }
269                TabState::None => {
270                    self.active.take();
271                }
272            }
273            self.id.request_all();
274            for (child, _) in self.children.iter().flatten() {
275                child.request_all();
276            }
277        }
278    }
279
280    fn style_pass(&mut self, _cx: &mut StyleCx<'_>) {
281        for (i, child) in self.id.children().into_iter().enumerate() {
282            match self.active {
283                Some(act_idx) if act_idx == i => {
284                    child.set_visible();
285                }
286                _ => {
287                    child.set_hidden();
288                }
289            }
290        }
291    }
292
293    fn paint(&mut self, cx: &mut crate::context::PaintCx) {
294        if let Some(active_tab) = self.active {
295            if let Some(Some((active, _))) = self
296                .children
297                .get(active_tab)
298                .or_else(|| self.children.first())
299            {
300                cx.paint_view(*active);
301            }
302        }
303    }
304}