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