Skip to main content

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
58    impl Events for Self {
59        const REDRAW_ON_MOUSE_OVER: bool = true;
60
61        type Data = ();
62
63        fn probe(&self, _: Coord) -> Id {
64            self.id()
65        }
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 ConfigCx, direction: Direction) {
143            if direction == self.direction {
144                return;
145            }
146
147            self.direction = direction;
148            cx.resize();
149        }
150
151        /// Call the handler `f` on page change
152        ///
153        /// `f` receives as parameters input data, page index and tab title.
154        #[inline]
155        #[must_use]
156        pub fn with(mut self, f: impl Fn(&mut EventCx, &A, usize, &str) + 'static) -> Self {
157            debug_assert!(self.on_change.is_none());
158            self.on_change = Some(Box::new(f));
159            self
160        }
161
162        /// Send the message generated by `f` on page change
163        ///
164        /// `f` receives as page index and tab title.
165        #[inline]
166        #[must_use]
167        pub fn with_msg<M>(self, f: impl Fn(usize, &str) -> M + 'static) -> Self
168        where
169            M: std::fmt::Debug + 'static,
170        {
171            self.with(move |cx, _, index, title| cx.push(f(index, title)))
172        }
173    }
174
175    impl Tile for Self {
176        fn nav_next(&self, reverse: bool, from: Option<usize>) -> Option<usize> {
177            let reverse = reverse ^ !self.direction.is_reversed();
178            kas::util::nav_next(reverse, from, self.child_indices())
179        }
180    }
181
182    impl Events for Self {
183        type Data = A;
184
185        fn handle_messages(&mut self, cx: &mut EventCx, data: &A) {
186            if let Some(SetIndex(index)) = cx.try_pop() {
187                self.set_active(cx, data, index);
188                if let Some(ref f) = self.on_change {
189                    let title = self.tabs.inner[index].as_str();
190                    f(cx, data, index, title);
191                }
192            }
193        }
194    }
195}
196
197impl<A> TabStack<A> {
198    /// Limit the number of pages considered and sized
199    ///
200    /// By default, this is `usize::MAX`: all pages are configured and affect
201    /// the stack's size requirements.
202    ///
203    /// Set this to 0 to avoid configuring all hidden pages.
204    /// Set this to `n` to configure the active page *and* the first `n` pages.
205    pub fn set_size_limit(&mut self, limit: usize) {
206        self.stack.set_size_limit(limit);
207    }
208
209    /// Limit the number of pages configured and sized (inline)
210    ///
211    /// By default, this is `usize::MAX`: all pages are configured and affect
212    /// the stack's size requirements.
213    ///
214    /// Set this to 0 to avoid configuring all hidden pages.
215    /// Set this to `n` to configure the active page *and* the first `n` pages.
216    pub fn with_size_limit(mut self, limit: usize) -> Self {
217        self.stack.set_size_limit(limit);
218        self
219    }
220
221    /// Get the index of the active page
222    #[inline]
223    pub fn active(&self) -> usize {
224        self.stack.active()
225    }
226
227    /// Set the active page (inline)
228    ///
229    /// Unlike [`Self::set_active`], this does not update anything; it is
230    /// assumed that sizing happens afterwards.
231    #[inline]
232    pub fn with_active(mut self, active: usize) -> Self {
233        self.stack = self.stack.with_active(active);
234        self
235    }
236
237    /// Set the active page
238    pub fn set_active(&mut self, cx: &mut ConfigCx, data: &A, index: usize) {
239        self.stack.set_active(cx, data, index);
240    }
241
242    /// Get a direct reference to the active child widget, if any
243    pub fn get_active(&self) -> Option<&Page<A>> {
244        self.stack.get_active()
245    }
246
247    /// True if there are no pages
248    pub fn is_empty(&self) -> bool {
249        self.stack.is_empty()
250    }
251
252    /// Returns the number of pages
253    pub fn len(&self) -> usize {
254        self.stack.len()
255    }
256
257    /// Remove all pages
258    ///
259    /// This does not change the active page index.
260    pub fn clear(&mut self) {
261        self.stack.clear();
262        self.tabs.inner.clear();
263    }
264
265    /// Get a page
266    pub fn get(&self, index: usize) -> Option<&Page<A>> {
267        self.stack.get(index)
268    }
269
270    /// Get a page
271    pub fn get_mut(&mut self, index: usize) -> Option<&mut Page<A>> {
272        self.stack.get_mut(index)
273    }
274
275    /// Get a tab
276    pub fn get_tab(&self, index: usize) -> Option<&Tab> {
277        self.tabs.inner.get(index)
278    }
279
280    /// Get a tab
281    pub fn get_tab_mut(&mut self, index: usize) -> Option<&mut Tab> {
282        self.tabs.inner.get_mut(index)
283    }
284
285    /// Append a page
286    ///
287    /// The new page is not made active (the active index may be changed to
288    /// avoid this). Consider calling [`Self::set_active`].
289    ///
290    /// Returns the new page's index.
291    pub fn push(&mut self, cx: &mut ConfigCx, data: &A, tab: Tab, page: Page<A>) -> usize {
292        let ti = self.tabs.inner.push(cx, &(), tab);
293        let si = self.stack.push(cx, data, page);
294        debug_assert_eq!(ti, si);
295        si
296    }
297
298    /// Remove the last child widget (if any) and return
299    ///
300    /// If this page was active then no page will be left active.
301    /// Consider also calling [`Self::set_active`].
302    pub fn pop(&mut self, cx: &mut ConfigCx) -> Option<(Tab, Page<A>)> {
303        let tab = self.tabs.inner.pop(cx);
304        let w = self.stack.pop(cx);
305        debug_assert_eq!(tab.is_some(), w.is_some());
306        tab.zip(w)
307    }
308
309    /// Inserts a child widget position `index`
310    ///
311    /// Panics if `index > len`.
312    ///
313    /// The active page does not change (the index of the active page may change instead).
314    pub fn insert(&mut self, cx: &mut ConfigCx, data: &A, index: usize, tab: Tab, page: Page<A>) {
315        self.tabs.inner.insert(cx, &(), index, tab);
316        self.stack.insert(cx, data, index, page);
317    }
318
319    /// Removes the child widget at position `index`
320    ///
321    /// Panics if `index` is out of bounds.
322    ///
323    /// If this page was active then no page will be left active.
324    /// Consider also calling [`Self::set_active`].
325    pub fn remove(&mut self, cx: &mut ConfigCx, index: usize) -> (Tab, Page<A>) {
326        let tab = self.tabs.inner.remove(cx, index);
327        let stack = self.stack.remove(cx, index);
328        (tab, stack)
329    }
330
331    /// Replace the child at `index`
332    ///
333    /// Panics if `index` is out of bounds.
334    ///
335    /// If the new child replaces the active page then a resize is triggered.
336    pub fn replace(&mut self, cx: &mut ConfigCx, data: &A, index: usize, page: Page<A>) -> Page<A> {
337        self.stack.replace(cx, data, index, page)
338    }
339
340    /// Append child widgets from an iterator
341    ///
342    /// The new pages are not made active (the active index may be changed to
343    /// avoid this). Consider calling [`Self::set_active`].
344    pub fn extend<T: IntoIterator<Item = (Tab, Page<A>)>>(
345        &mut self,
346        cx: &mut ConfigCx,
347        data: &A,
348        iter: T,
349    ) {
350        let iter = iter.into_iter();
351        // let min_len = iter.size_hint().0;
352        // self.tabs.reserve(min_len);
353        // self.stack.reserve(min_len);
354        for (tab, w) in iter {
355            self.tabs.inner.push(cx, &(), tab);
356            self.stack.push(cx, data, w);
357        }
358    }
359}
360
361impl<A, T, I> From<I> for TabStack<A>
362where
363    Tab: From<T>,
364    I: IntoIterator<Item = (T, Page<A>)>,
365{
366    #[inline]
367    fn from(iter: I) -> Self {
368        let iter = iter.into_iter();
369        let min_len = iter.size_hint().0;
370        let mut stack = Vec::with_capacity(min_len);
371        let mut tabs = Vec::with_capacity(min_len);
372        for (tab, w) in iter {
373            stack.push(w);
374            tabs.push(Tab::from(tab));
375        }
376        Self {
377            stack: Stack::from(stack),
378            tabs: Row::new(tabs).map_message(|index, Select| SetIndex(index)),
379            ..Default::default()
380        }
381    }
382}