kas_widgets/
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 stack
7
8use kas::layout::solve_size_rules;
9use kas::prelude::*;
10use std::collections::hash_map::{Entry, HashMap};
11use std::fmt::Debug;
12use std::ops::{Index, IndexMut};
13
14#[impl_self]
15mod Page {
16    /// A stack page (also known as a tab page)
17    #[widget]
18    #[layout(self.inner)]
19    pub struct Page<A> {
20        core: widget_core!(),
21        #[widget]
22        pub inner: Box<dyn Widget<Data = A>>,
23    }
24
25    impl Tile for Self {
26        fn role(&self, _: &mut dyn RoleCx) -> Role<'_> {
27            Role::TabPage
28        }
29    }
30
31    impl Events for Self {
32        type Data = A;
33    }
34
35    impl Self {
36        /// Construct from a widget
37        pub fn new(widget: impl Widget<Data = A> + 'static) -> Self {
38            Page::new_boxed(Box::new(widget))
39        }
40
41        /// Construct from a boxed widget
42        #[inline]
43        pub fn new_boxed(inner: Box<dyn Widget<Data = A>>) -> Self {
44            Page {
45                core: Default::default(),
46                inner,
47            }
48        }
49    }
50}
51
52#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
53enum State {
54    #[default]
55    None,
56    Configured,
57    Sized,
58}
59impl State {
60    fn is_configured(self) -> bool {
61        self != State::None
62    }
63}
64
65#[impl_self]
66mod Stack {
67    /// A stack of widgets
68    ///
69    /// A stack consists a set of child widgets, "pages", all of equal size.
70    /// Only a single page is visible at a time. The page is "turned" by calling
71    /// [`Self::set_active`].
72    ///
73    /// By default, all pages are configured and sized. To avoid configuring
74    /// hidden pages (thus preventing these pages from affecting size)
75    /// call [`Self::set_size_limit`] or [`Self::with_size_limit`].
76    ///
77    /// # Messages
78    ///
79    /// [`kas::messages::SetIndex`] may be used to change the page.
80    #[widget]
81    pub struct Stack<A> {
82        core: widget_core!(),
83        align_hints: AlignHints,
84        widgets: Vec<(Page<A>, State)>,
85        active: usize,
86        size_limit: usize,
87        next: usize,
88        id_map: HashMap<usize, usize>, // map key of Id to index
89    }
90
91    impl Default for Self {
92        fn default() -> Self {
93            Stack {
94                core: Default::default(),
95                align_hints: AlignHints::NONE,
96                widgets: Vec::new(),
97                active: 0,
98                size_limit: usize::MAX,
99                next: 0,
100                id_map: HashMap::new(),
101            }
102        }
103    }
104
105    impl Layout for Self {
106        fn size_rules(&mut self, sizer: SizeCx, axis: AxisInfo) -> SizeRules {
107            let mut rules = SizeRules::EMPTY;
108            for (index, entry) in self.widgets.iter_mut().enumerate() {
109                if index < self.size_limit || index == self.active {
110                    if entry.1.is_configured() {
111                        rules = rules.max(entry.0.size_rules(sizer.re(), axis));
112                        entry.1 = State::Sized;
113                    } else {
114                        entry.1 = State::None;
115                    }
116                } else {
117                    // Ensure entry will be resized before becoming active
118                    entry.1 = entry.1.min(State::Configured);
119                }
120            }
121            rules
122        }
123
124        fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect, hints: AlignHints) {
125            widget_set_rect!(rect);
126            self.align_hints = hints;
127            if let Some(entry) = self.widgets.get_mut(self.active) {
128                debug_assert_eq!(entry.1, State::Sized);
129                entry.0.set_rect(cx, rect, hints);
130            }
131        }
132
133        fn draw(&self, mut draw: DrawCx) {
134            if let Some(entry) = self.widgets.get(self.active) {
135                debug_assert_eq!(entry.1, State::Sized);
136                entry.0.draw(draw.re());
137            }
138        }
139    }
140
141    impl Tile for Self {
142        fn role(&self, _: &mut dyn RoleCx) -> Role<'_> {
143            Role::None
144        }
145
146        #[inline]
147        fn child_indices(&self) -> ChildIndices {
148            let mut end = self.active;
149            if self.active < self.widgets.len() {
150                end += 1;
151            }
152            (self.active..end).into()
153        }
154        fn get_child(&self, index: usize) -> Option<&dyn Tile> {
155            self.widgets.get(index).map(|(w, _)| w.as_tile())
156        }
157
158        fn find_child_index(&self, id: &Id) -> Option<usize> {
159            // Filter only returns Some(index) on configured children
160            id.next_key_after(self.id_ref())
161                .and_then(|k| self.id_map.get(&k).cloned())
162                .filter(|index| {
163                    self.widgets
164                        .get(*index)
165                        .map(|(_, state)| *state >= State::Configured)
166                        .unwrap_or(false)
167                })
168        }
169
170        fn nav_next(&self, _: bool, from: Option<usize>) -> Option<usize> {
171            let active = match from {
172                None => self.active,
173                Some(active) if active != self.active => self.active,
174                _ => return None,
175            };
176            if let Some(entry) = self.widgets.get(active) {
177                debug_assert_eq!(entry.1, State::Sized);
178                return Some(active);
179            }
180            None
181        }
182
183        fn probe(&self, coord: Coord) -> Id {
184            if let Some(entry) = self.widgets.get(self.active) {
185                debug_assert_eq!(entry.1, State::Sized);
186                if let Some(id) = entry.0.try_probe(coord) {
187                    return id;
188                }
189            }
190            self.id()
191        }
192    }
193
194    impl Events for Self {
195        fn make_child_id(&mut self, index: usize) -> Id {
196            if let Some((child, state)) = self.widgets.get(index) {
197                // Use the widget's existing identifier, if valid
198                if state.is_configured()
199                    && child.id_ref().is_valid()
200                    && self.id_ref().is_ancestor_of(child.id_ref())
201                {
202                    if let Some(key) = child.id_ref().next_key_after(self.id_ref()) {
203                        if let Entry::Vacant(entry) = self.id_map.entry(key) {
204                            entry.insert(index);
205                            return child.id();
206                        }
207                    }
208                }
209            }
210
211            loop {
212                let key = self.next;
213                self.next += 1;
214                if let Entry::Vacant(entry) = self.id_map.entry(key) {
215                    entry.insert(index);
216                    return self.id_ref().make_child(key);
217                }
218            }
219        }
220
221        fn configure(&mut self, _: &mut ConfigCx) {
222            // All children will be re-configured which will rebuild id_map
223            self.id_map.clear();
224        }
225
226        fn configure_recurse(&mut self, cx: &mut ConfigCx, data: &Self::Data) {
227            for index in 0..self.widgets.len() {
228                if index < self.size_limit || index == self.active {
229                    let id = self.make_child_id(index);
230                    let entry = &mut self.widgets[index];
231                    cx.configure(entry.0.as_node(data), id);
232                    if entry.1 == State::None {
233                        entry.1 = State::Configured;
234                    }
235                } else {
236                    // Ensure widget will be reconfigured before becoming active
237                    self.widgets[index].1 = State::None;
238                }
239            }
240        }
241
242        fn handle_messages(&mut self, cx: &mut EventCx, data: &A) {
243            if let Some(kas::messages::SetIndex(index)) = cx.try_pop() {
244                self.set_active(&mut cx.config_cx(), data, index);
245            }
246        }
247    }
248
249    impl Widget for Self {
250        type Data = A;
251
252        fn child_node<'n>(&'n mut self, data: &'n A, index: usize) -> Option<Node<'n>> {
253            self.widgets.get_mut(index).map(|(w, _)| w.as_node(data))
254        }
255    }
256
257    impl Index<usize> for Self {
258        type Output = Page<A>;
259
260        fn index(&self, index: usize) -> &Self::Output {
261            &self.widgets[index].0
262        }
263    }
264
265    impl IndexMut<usize> for Self {
266        fn index_mut(&mut self, index: usize) -> &mut Self::Output {
267            &mut self.widgets[index].0
268        }
269    }
270}
271
272impl<A> Stack<A> {
273    /// Construct a new, empty instance
274    ///
275    /// See also [`Stack::from`].
276    pub fn new() -> Self {
277        Stack::default()
278    }
279
280    /// Limit the number of pages considered and sized
281    ///
282    /// By default, this is `usize::MAX`: all pages are configured and affect
283    /// the stack's size requirements.
284    ///
285    /// Set this to 0 to avoid configuring all hidden pages.
286    /// Set this to `n` to configure the active page *and* the first `n` pages.
287    pub fn set_size_limit(&mut self, limit: usize) {
288        self.size_limit = limit;
289    }
290
291    /// Limit the number of pages configured and sized (inline)
292    ///
293    /// By default, this is `usize::MAX`: all pages are configured and affect
294    /// the stack's size requirements.
295    ///
296    /// Set this to 0 to avoid configuring all hidden pages.
297    /// Set this to `n` to configure the active page *and* the first `n` pages.
298    pub fn with_size_limit(mut self, limit: usize) -> Self {
299        self.size_limit = limit;
300        self
301    }
302
303    /// Get the index of the active widget
304    #[inline]
305    pub fn active(&self) -> usize {
306        self.active
307    }
308
309    /// Set the active widget (inline)
310    ///
311    /// Unlike [`Self::set_active`], this does not update anything; it is
312    /// assumed that this method is only used before the UI is run.
313    #[inline]
314    pub fn with_active(mut self, active: usize) -> Self {
315        debug_assert_eq!(
316            self.widgets
317                .get(self.active)
318                .map(|e| e.1)
319                .unwrap_or_default(),
320            State::None
321        );
322        self.active = active;
323        self
324    }
325
326    /// Set the active page
327    pub fn set_active(&mut self, cx: &mut ConfigCx, data: &A, index: usize) {
328        let old_index = self.active;
329        if old_index == index {
330            return;
331        }
332        self.active = index;
333
334        let rect = self.rect();
335        let id = self.make_child_id(index);
336        if let Some(entry) = self.widgets.get_mut(index) {
337            let node = entry.0.as_node(data);
338
339            if entry.1 == State::None {
340                cx.configure(node, id);
341                entry.1 = State::Configured;
342            } else {
343                cx.update(node);
344            }
345
346            if entry.1 == State::Configured {
347                let Size(w, _h) = rect.size;
348                // HACK: we should pass the known height here, but it causes
349                // even distribution of excess space. Maybe SizeRules::solve_seq
350                // should not always distribute excess space?
351                solve_size_rules(&mut entry.0, cx.size_cx(), Some(w), None);
352                entry.1 = State::Sized;
353            }
354
355            debug_assert_eq!(entry.1, State::Sized);
356            entry.0.set_rect(cx, rect, self.align_hints);
357            cx.region_moved();
358        } else {
359            if old_index < self.widgets.len() {
360                cx.region_moved();
361            }
362        }
363    }
364
365    /// Get a direct reference to the active child page, if any
366    pub fn get_active(&self) -> Option<&Page<A>> {
367        if self.active < self.widgets.len() {
368            Some(&self.widgets[self.active].0)
369        } else {
370            None
371        }
372    }
373
374    /// True if there are no pages
375    pub fn is_empty(&self) -> bool {
376        self.widgets.is_empty()
377    }
378
379    /// Returns the number of pages
380    pub fn len(&self) -> usize {
381        self.widgets.len()
382    }
383
384    /// Remove all pages
385    ///
386    /// This does not change the activen page index.
387    pub fn clear(&mut self) {
388        self.widgets.clear();
389    }
390
391    /// Returns a reference to the page, if any
392    pub fn get(&self, index: usize) -> Option<&Page<A>> {
393        self.widgets.get(index).map(|e| &e.0)
394    }
395
396    /// Returns a mutable reference to the page, if any
397    pub fn get_mut(&mut self, index: usize) -> Option<&mut Page<A>> {
398        self.widgets.get_mut(index).map(|e| &mut e.0)
399    }
400
401    /// Configure and size the page at index
402    fn configure_and_size(&mut self, cx: &mut ConfigCx, data: &A, index: usize) {
403        let Size(w, h) = self.rect().size;
404        let id = self.make_child_id(index);
405        if let Some(entry) = self.widgets.get_mut(index) {
406            cx.configure(entry.0.as_node(data), id);
407            solve_size_rules(&mut entry.0, cx.size_cx(), Some(w), Some(h));
408            entry.1 = State::Sized;
409        }
410    }
411
412    /// Append a page
413    ///
414    /// The new page is not made active (the active index may be changed to
415    /// avoid this). Consider calling [`Self::set_active`].
416    ///
417    /// Returns the new page's index.
418    pub fn push(&mut self, cx: &mut ConfigCx, data: &A, page: Page<A>) -> usize {
419        let index = self.widgets.len();
420        if index == self.active {
421            self.active = usize::MAX;
422        }
423        self.widgets.push((page, State::None));
424
425        if index < self.size_limit {
426            self.configure_and_size(cx, data, index);
427        }
428        index
429    }
430
431    /// Remove the last child widget (if any) and return
432    ///
433    /// If this page was active then no page will be left active.
434    /// Consider also calling [`Self::set_active`].
435    pub fn pop(&mut self, cx: &mut EventState) -> Option<Page<A>> {
436        let result = self.widgets.pop().map(|(w, _)| w);
437        if let Some(w) = result.as_ref() {
438            if self.active > 0 && self.active == self.widgets.len() {
439                cx.region_moved();
440            }
441
442            if w.id_ref().is_valid() {
443                if let Some(key) = w.id_ref().next_key_after(self.id_ref()) {
444                    self.id_map.remove(&key);
445                }
446            }
447        }
448        result
449    }
450
451    /// Inserts a child widget position `index`
452    ///
453    /// Panics if `index > len`.
454    ///
455    /// The active page does not change (the index of the active page may change instead).
456    pub fn insert(&mut self, cx: &mut ConfigCx, data: &A, index: usize, page: Page<A>) {
457        if self.active >= index {
458            self.active = self.active.saturating_add(1);
459        }
460
461        self.widgets.insert(index, (page, State::None));
462
463        for v in self.id_map.values_mut() {
464            if *v >= index {
465                *v += 1;
466            }
467        }
468
469        if index < self.size_limit {
470            self.configure_and_size(cx, data, index);
471        }
472    }
473
474    /// Removes the child widget at position `index`
475    ///
476    /// Panics if `index` is out of bounds.
477    ///
478    /// If this page was active then no page will be left active.
479    /// Consider also calling [`Self::set_active`].
480    pub fn remove(&mut self, cx: &mut EventState, index: usize) -> Page<A> {
481        let (w, _) = self.widgets.remove(index);
482        if w.id_ref().is_valid() {
483            if let Some(key) = w.id_ref().next_key_after(self.id_ref()) {
484                self.id_map.remove(&key);
485            }
486        }
487
488        if self.active == index {
489            self.active = usize::MAX;
490            cx.region_moved();
491        }
492
493        for v in self.id_map.values_mut() {
494            if *v > index {
495                *v -= 1;
496            }
497        }
498        w
499    }
500
501    /// Replace the child at `index`
502    ///
503    /// Panics if `index` is out of bounds.
504    ///
505    /// If the new child replaces the active page then [`Action::RESIZE`] is triggered.
506    pub fn replace(
507        &mut self,
508        cx: &mut ConfigCx,
509        data: &A,
510        index: usize,
511        mut page: Page<A>,
512    ) -> Page<A> {
513        let entry = &mut self.widgets[index];
514        std::mem::swap(&mut page, &mut entry.0);
515        entry.1 = State::None;
516
517        if page.id_ref().is_valid() {
518            if let Some(key) = page.id_ref().next_key_after(self.id_ref()) {
519                self.id_map.remove(&key);
520            }
521        }
522
523        if index < self.size_limit || index == self.active {
524            self.configure_and_size(cx, data, index);
525        }
526
527        if index == self.active {
528            cx.resize(self);
529        }
530
531        page
532    }
533
534    /// Append child widgets from an iterator
535    ///
536    /// The new pages are not made active (the active index may be changed to
537    /// avoid this). Consider calling [`Self::set_active`].
538    pub fn extend<T: IntoIterator<Item = Page<A>>>(
539        &mut self,
540        cx: &mut ConfigCx,
541        data: &A,
542        iter: T,
543    ) {
544        let old_len = self.widgets.len();
545        let iter = iter.into_iter();
546        if let Some(ub) = iter.size_hint().1 {
547            self.widgets.reserve(ub);
548        }
549        for w in iter {
550            let index = self.widgets.len();
551            self.widgets.push((w, State::None));
552            if index < self.size_limit {
553                self.configure_and_size(cx, data, index);
554            }
555        }
556
557        if (old_len..self.widgets.len()).contains(&self.active) {
558            self.active = usize::MAX;
559        }
560    }
561
562    /// Resize, using the given closure to construct new widgets
563    ///
564    /// The new pages are not made active (the active index may be changed to
565    /// avoid this). Consider calling [`Self::set_active`].
566    pub fn resize_with<F: Fn(usize) -> Page<A>>(
567        &mut self,
568        cx: &mut ConfigCx,
569        data: &A,
570        len: usize,
571        f: F,
572    ) {
573        let old_len = self.widgets.len();
574
575        if len < old_len {
576            loop {
577                let (w, _) = self.widgets.pop().unwrap();
578                if w.id_ref().is_valid() {
579                    if let Some(key) = w.id_ref().next_key_after(self.id_ref()) {
580                        self.id_map.remove(&key);
581                    }
582                }
583                if len == self.widgets.len() {
584                    return;
585                }
586            }
587        }
588
589        if len > old_len {
590            self.widgets.reserve(len - old_len);
591            for index in old_len..len {
592                self.widgets.push((f(index), State::None));
593                if index < self.size_limit {
594                    self.configure_and_size(cx, data, index);
595                }
596            }
597
598            if (old_len..len).contains(&self.active) {
599                self.active = usize::MAX;
600            }
601        }
602    }
603}
604
605impl<A, I> From<I> for Stack<A>
606where
607    I: IntoIterator<Item = Page<A>>,
608{
609    #[inline]
610    fn from(iter: I) -> Self {
611        Self {
612            widgets: iter.into_iter().map(|w| (w, State::None)).collect(),
613            ..Default::default()
614        }
615    }
616}