kas_widgets/
tab_stack.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! A tabbed stack
7
8use crate::adapt::{AdaptEvents, AdaptWidget};
9use crate::{AccessLabel, Page, Row, Stack};
10use kas::messages::{Select, SetIndex};
11use kas::prelude::*;
12use kas::theme::FrameStyle;
13
14#[impl_self]
15mod Tab {
16    /// A tab
17    ///
18    /// This is a special variant of `Button` which sends a [`Select`] on press.
19    ///
20    /// # Messages
21    ///
22    /// [`kas::messages::Activate`] may be used to open this tab.
23    #[widget]
24    #[layout(frame!(self.label).with_style(FrameStyle::Tab))]
25    pub struct Tab {
26        core: widget_core!(),
27        #[widget]
28        label: AccessLabel,
29    }
30
31    impl Self {
32        /// Construct a button with given `label` widget
33        #[inline]
34        pub fn new(label: impl Into<AccessString>) -> Self {
35            Tab {
36                core: Default::default(),
37                label: AccessLabel::new(label),
38            }
39        }
40
41        /// Get text contents
42        pub fn as_str(&self) -> &str {
43            self.label.as_str()
44        }
45    }
46
47    impl Tile for Self {
48        fn navigable(&self) -> bool {
49            true
50        }
51
52        fn role(&self, cx: &mut dyn RoleCx) -> Role<'_> {
53            cx.set_label(self.label.id());
54            Role::Tab
55        }
56
57        fn probe(&self, _: Coord) -> Id {
58            self.id()
59        }
60    }
61
62    impl Events for Self {
63        const REDRAW_ON_MOUSE_OVER: bool = true;
64
65        type Data = ();
66
67        fn handle_event(&mut self, cx: &mut EventCx, _: &(), event: Event) -> IsUsed {
68            event.on_click(cx, self.id(), |cx| cx.push(Select))
69        }
70
71        fn handle_messages(&mut self, cx: &mut EventCx, _: &()) {
72            if let Some(kas::messages::Activate(code)) = cx.try_pop() {
73                cx.push(Select);
74                cx.depress_with_key(&self, code);
75            }
76        }
77    }
78
79    impl<T: Into<AccessString>> From<T> for Tab {
80        fn from(label: T) -> Self {
81            Tab::new(label)
82        }
83    }
84}
85
86/// A tabbed stack of boxed widgets
87///
88/// This is a parametrisation of [`TabStack`].
89pub type BoxTabStack<Data> = TabStack<Box<dyn Widget<Data = Data>>>;
90
91#[impl_self]
92mod TabStack {
93    /// A tabbed stack of widgets
94    ///
95    /// A stack consists a set of child widgets, "pages", all of equal size.
96    /// Only a single page is visible at a time. The page is "turned" via tab
97    /// handles or calling [`Self::set_active`].
98    ///
99    /// Type parameter `D` controls the position of tabs relative to the stack;
100    /// default value is [`Direction::Up`]: tabs are positioned above the stack.
101    /// Within the bar, items are always left-to-right
102    /// (TODO: support for vertical bars).
103    ///
104    /// This may only be parametrised with a single widget type, thus usually
105    /// it will be necessary to box children (this is what [`BoxTabStack`] is).
106    ///
107    /// See also the main implementing widget: [`Stack`].
108    ///
109    /// # Messages
110    ///
111    /// [`kas::messages::SetIndex`] may be used to change the page.
112    #[impl_default(Self::new())]
113    #[widget]
114    #[layout(list![self.stack, self.tabs].with_direction(self.direction))]
115    pub struct TabStack<A> {
116        core: widget_core!(),
117        direction: Direction,
118        #[widget(&())]
119        tabs: AdaptEvents<Row<Vec<Tab>>>, // TODO: want a TabBar widget for scrolling support?
120        #[widget]
121        stack: Stack<A>,
122        on_change: Option<Box<dyn Fn(&mut EventCx, &A, usize, &str)>>,
123    }
124
125    impl Self {
126        /// Construct a new, empty instance
127        ///
128        /// See also [`TabStack::from`].
129        pub fn new() -> Self {
130            Self {
131                core: Default::default(),
132                direction: Direction::Up,
133                stack: Stack::new(),
134                tabs: Row::new(vec![]).map_message(|index, Select| SetIndex(index)),
135                on_change: None,
136            }
137        }
138
139        /// Set the position of tabs relative to content
140        ///
141        /// Default value: [`Direction::Up`]
142        pub fn set_direction(&mut self, cx: &mut EventState, direction: Direction) {
143            if direction == self.direction {
144                return;
145            }
146
147            self.direction = direction;
148            // Note: most of the time Action::SET_RECT would be enough, but margins can be different
149            cx.resize(self);
150        }
151
152        /// Call the handler `f` on page change
153        ///
154        /// `f` receives as parameters input data, page index and tab title.
155        #[inline]
156        #[must_use]
157        pub fn with(mut self, f: impl Fn(&mut EventCx, &A, usize, &str) + 'static) -> Self {
158            debug_assert!(self.on_change.is_none());
159            self.on_change = Some(Box::new(f));
160            self
161        }
162
163        /// Send the message generated by `f` on page change
164        ///
165        /// `f` receives as page index and tab title.
166        #[inline]
167        #[must_use]
168        pub fn with_msg<M>(self, f: impl Fn(usize, &str) -> M + 'static) -> Self
169        where
170            M: std::fmt::Debug + 'static,
171        {
172            self.with(move |cx, _, index, title| cx.push(f(index, title)))
173        }
174    }
175
176    impl Tile for Self {
177        fn nav_next(&self, reverse: bool, from: Option<usize>) -> Option<usize> {
178            let reverse = reverse ^ !self.direction.is_reversed();
179            kas::util::nav_next(reverse, from, self.child_indices())
180        }
181    }
182
183    impl Events for Self {
184        type Data = A;
185
186        fn handle_messages(&mut self, cx: &mut EventCx, data: &A) {
187            if let Some(SetIndex(index)) = cx.try_pop() {
188                self.set_active(&mut cx.config_cx(), data, index);
189                if let Some(ref f) = self.on_change {
190                    let title = self.tabs.inner[index].as_str();
191                    f(cx, data, index, title);
192                }
193            }
194        }
195    }
196}
197
198impl<A> TabStack<A> {
199    /// Limit the number of pages considered and sized
200    ///
201    /// By default, this is `usize::MAX`: all pages are configured and affect
202    /// the stack's size requirements.
203    ///
204    /// Set this to 0 to avoid configuring all hidden pages.
205    /// Set this to `n` to configure the active page *and* the first `n` pages.
206    pub fn set_size_limit(&mut self, limit: usize) {
207        self.stack.set_size_limit(limit);
208    }
209
210    /// Limit the number of pages configured and sized (inline)
211    ///
212    /// By default, this is `usize::MAX`: all pages are configured and affect
213    /// the stack's size requirements.
214    ///
215    /// Set this to 0 to avoid configuring all hidden pages.
216    /// Set this to `n` to configure the active page *and* the first `n` pages.
217    pub fn with_size_limit(mut self, limit: usize) -> Self {
218        self.stack.set_size_limit(limit);
219        self
220    }
221
222    /// Get the index of the active page
223    #[inline]
224    pub fn active(&self) -> usize {
225        self.stack.active()
226    }
227
228    /// Set the active page (inline)
229    ///
230    /// Unlike [`Self::set_active`], this does not update anything; it is
231    /// assumed that sizing happens afterwards.
232    #[inline]
233    pub fn with_active(mut self, active: usize) -> Self {
234        self.stack = self.stack.with_active(active);
235        self
236    }
237
238    /// Set the active page
239    pub fn set_active(&mut self, cx: &mut ConfigCx, data: &A, index: usize) {
240        self.stack.set_active(cx, data, index);
241    }
242
243    /// Get a direct reference to the active child widget, if any
244    pub fn get_active(&self) -> Option<&Page<A>> {
245        self.stack.get_active()
246    }
247
248    /// True if there are no pages
249    pub fn is_empty(&self) -> bool {
250        self.stack.is_empty()
251    }
252
253    /// Returns the number of pages
254    pub fn len(&self) -> usize {
255        self.stack.len()
256    }
257
258    /// Remove all pages
259    ///
260    /// This does not change the active page index.
261    pub fn clear(&mut self) {
262        self.stack.clear();
263        self.tabs.inner.clear();
264    }
265
266    /// Get a page
267    pub fn get(&self, index: usize) -> Option<&Page<A>> {
268        self.stack.get(index)
269    }
270
271    /// Get a page
272    pub fn get_mut(&mut self, index: usize) -> Option<&mut Page<A>> {
273        self.stack.get_mut(index)
274    }
275
276    /// Get a tab
277    pub fn get_tab(&self, index: usize) -> Option<&Tab> {
278        self.tabs.inner.get(index)
279    }
280
281    /// Get a tab
282    pub fn get_tab_mut(&mut self, index: usize) -> Option<&mut Tab> {
283        self.tabs.inner.get_mut(index)
284    }
285
286    /// Append a page
287    ///
288    /// The new page is not made active (the active index may be changed to
289    /// avoid this). Consider calling [`Self::set_active`].
290    ///
291    /// Returns the new page's index.
292    pub fn push(&mut self, cx: &mut ConfigCx, data: &A, tab: Tab, page: Page<A>) -> usize {
293        let ti = self.tabs.inner.push(cx, &(), tab);
294        let si = self.stack.push(cx, data, page);
295        debug_assert_eq!(ti, si);
296        si
297    }
298
299    /// Remove the last child widget (if any) and return
300    ///
301    /// If this page was active then no page will be left active.
302    /// Consider also calling [`Self::set_active`].
303    pub fn pop(&mut self, cx: &mut EventState) -> Option<(Tab, Page<A>)> {
304        let tab = self.tabs.inner.pop(cx);
305        let w = self.stack.pop(cx);
306        debug_assert_eq!(tab.is_some(), w.is_some());
307        tab.zip(w)
308    }
309
310    /// Inserts a child widget position `index`
311    ///
312    /// Panics if `index > len`.
313    ///
314    /// The active page does not change (the index of the active page may change instead).
315    pub fn insert(&mut self, cx: &mut ConfigCx, data: &A, index: usize, tab: Tab, page: Page<A>) {
316        self.tabs.inner.insert(cx, &(), index, tab);
317        self.stack.insert(cx, data, index, page);
318    }
319
320    /// Removes the child widget at position `index`
321    ///
322    /// Panics if `index` is out of bounds.
323    ///
324    /// If this page was active then no page will be left active.
325    /// Consider also calling [`Self::set_active`].
326    pub fn remove(&mut self, cx: &mut EventState, index: usize) -> (Tab, Page<A>) {
327        let tab = self.tabs.inner.remove(cx, index);
328        let stack = self.stack.remove(cx, index);
329        (tab, stack)
330    }
331
332    /// Replace the child at `index`
333    ///
334    /// Panics if `index` is out of bounds.
335    ///
336    /// If the new child replaces the active page then [`Action::RESIZE`] is triggered.
337    pub fn replace(&mut self, cx: &mut ConfigCx, data: &A, index: usize, page: Page<A>) -> Page<A> {
338        self.stack.replace(cx, data, index, page)
339    }
340
341    /// Append child widgets from an iterator
342    ///
343    /// The new pages are not made active (the active index may be changed to
344    /// avoid this). Consider calling [`Self::set_active`].
345    pub fn extend<T: IntoIterator<Item = (Tab, Page<A>)>>(
346        &mut self,
347        cx: &mut ConfigCx,
348        data: &A,
349        iter: T,
350    ) {
351        let iter = iter.into_iter();
352        // let min_len = iter.size_hint().0;
353        // self.tabs.reserve(min_len);
354        // self.stack.reserve(min_len);
355        for (tab, w) in iter {
356            self.tabs.inner.push(cx, &(), tab);
357            self.stack.push(cx, data, w);
358        }
359    }
360}
361
362impl<A, T, I> From<I> for TabStack<A>
363where
364    Tab: From<T>,
365    I: IntoIterator<Item = (T, Page<A>)>,
366{
367    #[inline]
368    fn from(iter: I) -> Self {
369        let iter = iter.into_iter();
370        let min_len = iter.size_hint().0;
371        let mut stack = Vec::with_capacity(min_len);
372        let mut tabs = Vec::with_capacity(min_len);
373        for (tab, w) in iter {
374            stack.push(w);
375            tabs.push(Tab::from(tab));
376        }
377        Self {
378            stack: Stack::from(stack),
379            tabs: Row::new(tabs).map_message(|index, Select| SetIndex(index)),
380            ..Default::default()
381        }
382    }
383}