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}